Skip to content

Commit

Permalink
[loop_macro] Add support for iteration limits
Browse files Browse the repository at this point in the history
Loop macros have the potential of running forever and locking up
Klipper completely. This commit attemtps to improve this by providing
a way for loop macros to define a safety switch by limiting the number
of iterations the macro can go through.

While it is still possible to lock up Klipper if limits are not defined,
if a limit is in place, a loop macro will terminate even if it does not
reach a `BREAK` command.
  • Loading branch information
voidtrance committed Nov 29, 2023
1 parent 77e22a6 commit 282e1bf
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 5 deletions.
74 changes: 69 additions & 5 deletions loop_macro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ including delayed G-Code. This is due to the fact that loop macros
hold the G-Code execution lock while running, which prevents all other
G-Code from executing.

## Usage
## Configuration
The `loop_macro` extension enables a new section (`[loop_macro MACRO_NAME]`)
with the following configuration:

Expand All @@ -44,6 +44,11 @@ with the following configuration:
# A list of G-Code commands to execute after the completion of the
# looping commands in `gcode`. See docs/Command_Templates.md for
# G-Code format.
#iteration_limit:
# The maximum number of time the macro will execute. This can be
# used in order to avoid infinite loops. If the macro reaches this
# number of iterations without hiting a termination point, it will
# stop execution. Default is 0 (no limit).
#variable_<name>:
# One may specify any number of options with a "variable_" prefix.
# The given variable name will be assigned the given value (parsed
Expand All @@ -65,11 +70,33 @@ with the following configuration:
# using the auto completion feature. Default "G-Code macro"
```

The `gcode` template supports two special commands - `CONTINUE` and `BREAK`.
Each of the GCode templates (`entry`, `gcode`, and `exit`) can make use of
the following variables, which are defined by the loop macro, itself:

| Variable | Description |
| :- | :- |
| `iter` | The current iteration count. |
| `limit` | The maximum iteration limit. |

In addition, the `gcode` template supports two special commands - `CONTINUE`
and `BREAK`:

* `CONTINUE` stops the execution of the current loop interation and jumps to
the next iteration.
* `BREAK` terminates the entire loop.

## Usage
Loop macros are used just like any other GCode macro defined with the
`gcode_macro` section.

In addition to the normal macro parameters supported by GCode macros, loop
macros also accept the `LIMIT` parameter, which can be used to change the
maximum iteration count. Note that the `LIMIT` parameter is not available
in the `params` object like other macro parameters as it is handled internally. Instead, the value can be obtained through the `limit` variable provided by `loop_macro`.

> [!note]
> In order to prevent infinite loops, a `LIMIT` value of `0` is ignored.
## Examples
The following is a simple example that prints a message to the console
until the `count` variable reaches the value 5:
Expand Down Expand Up @@ -151,18 +178,55 @@ BREAK

and the loop macro terminates.

The next example show the use of the `LIMIT` parameter:
```ini
[loop_macro MY_LOOP_MACRO]
iteration_limit: 5
entry:
RESPOND MSG="Iteration limit: {limit}"
gcode:
RESPOND MSG="Current iteration: {iter} out of {limit}
```
If the above loop macro is called without the `LIMIT` parameter, it will execute
`iteration_limit` number of times, producing the following output:
```
echo: Iteration limit: 5
echo: Current iteration: 0 out of 5
echo: Current iteration: 1 out of 5
echo: Current iteration: 2 out of 5
echo: Current iteration: 3 out of 5
echo: Current iteration: 4 out of 5
```
However, if the same macro is executed with the command `MY_LOOP_MACRO LIMIT=7`, the
output changes to:
```
echo: Iteration limit: 7
echo: Current iteration: 0 out of 7
echo: Current iteration: 1 out of 7
echo: Current iteration: 2 out of 7
echo: Current iteration: 3 out of 7
echo: Current iteration: 4 out of 7
echo: Current iteration: 5 out of 7
echo: Current iteration: 6 out of 7
```
## WARNING!!!! WARNING!!!! WARNING!!!
> [!warning]
> [!caution]
> **Care must be taken when using loop macros in order to avoid locking up Klipper!**
Due to the fact that loop macros take the G-Code execution lock and loop macros don't have
a built-in termination mechanism, it is possible (and easy) to write a macro that loops
forever.
A loop macro will only terminate if it encounters the `BREAK` command at some point
of its execution. What this means is that a condition must exist under which the `BREAK`
command will be executed.
of its execution or its iteration limit is reached. What this means is that:
* either a condition must exist under which the `BREAK` command will be executed, or
* the loop macro must set a non-zero value for `iteration_limit` in its configuration.
A good example of this is the `MY_TEMPERATURE_WAIT` loop macro above. While the `BREAK`
special command does appear in the looping G-Code template, the processing will never
Expand Down
19 changes: 19 additions & 0 deletions loop_macro/loop_macro.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(self, config):
macro_obj = self.printer.load_object(config, 'gcode_macro')
self.entry_template = macro_obj.load_template(config, 'entry', '')
self.exit_template = macro_obj.load_template(config, 'exit', '')
self.iteration_limit = config.getint("iteration_limit", 0)

def _create_context(self, gcmd, template):
context = dict(self.variables)
Expand All @@ -25,6 +26,21 @@ def cmd(self, gcmd):
if self.printer.is_shutdown():
return

limit = gcmd.get_int("LIMIT", None)
if limit is None:
limit = self.iteration_limit

# LIMIT is a special argument that is provided by the
# implementation. So, reach into the GCode command
# parameters and remove it.
gcmd._params.pop("LIMIT", None)
parts = gcmd._commandline.split()
parts = [x for x in parts if not x.startswith("LIMIT")]
gcmd._commandline = " ".join(parts)

self.variables["iter"] = 0
self.variables["limit"] = limit

context = self._create_context(gcmd, self.entry_template)
self.entry_template.run_gcode_from_command(context)

Expand All @@ -40,6 +56,9 @@ def cmd(self, gcmd):
stop_execution = True
break
self.gcode.run_script_from_command(gcode)
self.variables["iter"] += 1
if limit > 0 and self.variables["iter"] >= limit:
break

context = self._create_context(gcmd, self.exit_template)
self.exit_template.run_gcode_from_command(context)
Expand Down

0 comments on commit 282e1bf

Please sign in to comment.