Skip to content

Commit

Permalink
arksine/dw-0 gcode_shell_command
Browse files Browse the repository at this point in the history
  • Loading branch information
rogerlz committed Oct 18, 2023
1 parent 72b9b99 commit 97071d5
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Features merged into the master branch:

- [gcode: Jinja2.ext.do extension](https://github.com/DangerKlippers/danger-klipper/pull/26) ([klipper#5149](https://github.com/Klipper3d/klipper/pull/5149))

- [gcode: gcode_shell_command](https://github.com/DangerKlippers/danger-klipper/pull/26) ([klipper#2173](https://github.com/Klipper3d/klipper/pull/2173) / [kiuah](https://github.com/dw-0/kiauh/blob/master/resources/gcode_shell_command.py) )

- [probe: Dockable Probe](https://github.com/DangerKlippers/danger-klipper/pull/43) ([klipper#4328](https://github.com/Klipper3d/klipper/pull/4328))

- [probe: Drop the first result](https://github.com/DangerKlippers/danger-klipper/pull/2) ([klipper#3397](https://github.com/Klipper3d/klipper/issues/3397))
Expand Down
77 changes: 77 additions & 0 deletions docs/G-Code_Shell_Command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# G-Code Shell Command Extension

### Creator of this extension is [Arksine](https://github.com/Arksine).

This is a brief explanation of how to use the shell command extension for Klipper, which you can install with KIAUH.

After installing the extension you can execute linux commands or even scripts from within Klipper with custom commands defined in your printer.cfg.

#### How to configure a shell command:

```shell
# Runs a linux command or script from within klipper. Note that sudo commands
# that require password authentication are disallowed. All executable scripts
# should include a shebang.
# [gcode_shell_command my_shell_cmd]
#command:
# The linux shell command/script to be executed. This parameter must be
# provided
#timeout: 2.
# The timeout in seconds until the command is forcably terminated. Default
# is 2 seconds.
#verbose: True
# If enabled, the command's output will be forwarded to the terminal. Its
# recommended to set this to false for commands that my run in quick
# succession. Default is True.
```

Once you have set up a shell command with the given parameters from above in your printer.cfg you can run the command as follows:
`RUN_SHELL_COMMAND CMD=name`

Example:

```
[gcode_shell_command hello_world]
command: echo hello world
timeout: 2.
verbose: True
```

Execute with:
`RUN_SHELL_COMMAND CMD=hello_world`

### Passing parameters:

As of commit [f231fa9](https://github.com/dw-0/kiauh/commit/f231fa9c69191f23277b4e3319f6b675bfa0ee42) it is also possible to pass optional parameters to a `gcode_shell_command`.
The following short example shows storing the extruder temperature into a variable, passing that value with a parameter to a `gcode_shell_command`, which then,
once the gcode_macro runs and the gcode_shell_command gets called, executes the `script.sh`. The script then echoes a message to the console (if `verbose: True`)
and writes the value of the parameter into a textfile called `test.txt` located in the home directory.

Content of the `gcode_shell_command` and the `gcode_macro`:

```
[gcode_shell_command print_to_file]
command: sh /home/pi/klipper_config/script.sh
timeout: 30.
verbose: True
[gcode_macro GET_TEMP]
gcode:
{% set temp = printer.extruder.temperature %}
{ action_respond_info("%s" % (temp)) }
RUN_SHELL_COMMAND CMD=print_to_file PARAMS={temp}
```

Content of `script.sh`:

```shell
#!/bin/sh

echo "temp is: $1"
echo "$1" >> "${HOME}/test.txt"
```

## Warning

This extension may have a high potential for abuse if not used carefully! Also, depending on the command you execute, high system loads may occur and can cause system instabilities.
Use this extension at your own risk and only if you know what you are doing!
96 changes: 96 additions & 0 deletions klippy/extras/gcode_shell_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Run a shell command via gcode
#
# Copyright (C) 2019 Eric Callahan <[email protected]>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os
import shlex
import subprocess
import logging


class ShellCommand:
def __init__(self, config):
self.name = config.get_name().split()[-1]
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object("gcode")
cmd = config.get("command")
cmd = os.path.expanduser(cmd)
self.command = shlex.split(cmd)
self.timeout = config.getfloat("timeout", 2.0, above=0.0)
self.verbose = config.getboolean("verbose", True)
self.proc_fd = None
self.partial_output = ""
self.gcode.register_mux_command(
"RUN_SHELL_COMMAND",
"CMD",
self.name,
self.cmd_RUN_SHELL_COMMAND,
desc=self.cmd_RUN_SHELL_COMMAND_help,
)

def _process_output(self, eventime):
if self.proc_fd is None:
return
try:
data = os.read(self.proc_fd, 4096)
except Exception:
pass
data = self.partial_output + data.decode()
if "\n" not in data:
self.partial_output = data
return
elif data[-1] != "\n":
split = data.rfind("\n") + 1
self.partial_output = data[split:]
data = data[:split]
else:
self.partial_output = ""
self.gcode.respond_info(data)

cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command"

def cmd_RUN_SHELL_COMMAND(self, params):
gcode_params = params.get("PARAMS", "")
gcode_params = shlex.split(gcode_params)
reactor = self.printer.get_reactor()
try:
proc = subprocess.Popen(
self.command + gcode_params,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
except Exception:
logging.exception(
"shell_command: Command {%s} failed" % (self.name)
)
raise self.gcode.error("Error running command {%s}" % (self.name))
if self.verbose:
self.proc_fd = proc.stdout.fileno()
self.gcode.respond_info("Running Command {%s}...:" % (self.name))
hdl = reactor.register_fd(self.proc_fd, self._process_output)
eventtime = reactor.monotonic()
endtime = eventtime + self.timeout
complete = False
while eventtime < endtime:
eventtime = reactor.pause(eventtime + 0.05)
if proc.poll() is not None:
complete = True
break
if not complete:
proc.terminate()
if self.verbose:
if self.partial_output:
self.gcode.respond_info(self.partial_output)
self.partial_output = ""
if complete:
msg = "Command {%s} finished\n" % (self.name)
else:
msg = "Command {%s} timed out" % (self.name)
self.gcode.respond_info(msg)
reactor.unregister_fd(hdl)
self.proc_fd = None


def load_config_prefix(config):
return ShellCommand(config)
13 changes: 13 additions & 0 deletions test/klippy/gcode_shell_command.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Test case for gcode_shell_command
[mcu]
serial: /dev/ttyACM0

[printer]
kinematics: none
max_velocity: 300
max_accel: 3000

[gcode_shell_command HELLO_WORLD]
command: echo hello world
timeout: 2.
verbose: True
5 changes: 5 additions & 0 deletions test/klippy/gcode_shell_command.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Test case for gcode_shell_command
CONFIG gcode_shell_command.cfg
DICTIONARY atmega2560.dict

HELLO_WORLD

0 comments on commit 97071d5

Please sign in to comment.