Skip to content

Commit

Permalink
Configurable GCODE override in print files
Browse files Browse the repository at this point in the history
  • Loading branch information
smartin015 committed Dec 29, 2022
1 parent 1e6e83b commit 4950f39
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 0 deletions.
1 change: 1 addition & 0 deletions continuousprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def on_startup(self, host=None, port=None):
)

def on_after_startup(self):
self._plugin.patchCommJobReader()
self._plugin.start()

# It's possible to miss events or for some weirdness to occur in conditionals. Adding a watchdog
Expand Down
1 change: 1 addition & 0 deletions continuousprint/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Keys(Enum):
) # One of "do_nothing", "add_draft", "add_printable"
INFER_PROFILE = ("cp_infer_profile", True)
AUTO_RECONNECT = ("cp_auto_reconnect", False)
SKIP_GCODE_COMMANDS = ("cp_skip_gcode_commands", "")

def __init__(self, setting, default):
self.setting = setting
Expand Down
48 changes: 48 additions & 0 deletions continuousprint/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,54 @@ def _setup_thirdparty_plugin_integration(self):
octoprint.events.Events, "PLUGIN__SPOOLMANAGER_SPOOL_DESELECTED", None
)

def patchCommJobReader(self):
# Patch the comms interface to allow for suppressing GCODE script events when the
# queue is running script events
try:
if self._get_key(Keys.SKIP_GCODE_COMMANDS).strip() == "":
self._logger.info(
"Skipping patch of comm._get_next_from_job; no commands configured to skip"
)
return
self._jobCommReaderOrig = self._printer._comm._get_next_from_job
self._printer._comm._get_next_from_job = self.gatedCommJobReader
self._logger.info("Patched comm._get_next_from_job")
except Exception:
self._logger.error(traceback.format_exc())

def gatedCommJobReader(self, *args, **kwargs):
# As this patches core OctoPrint functionality, we wrap *everything*
# in try/catch to ensure it continues to execute if CPQ raises an exception.
result = self._jobCommReaderOrig(*args, **kwargs)
try:
# Only mess with gcode commands of printed files, not events
if self.d.state != self.d._state_printing:
return result

cmd_list = set(
[
c.split(";", 1)[0].strip().upper()
for c in self._get_key(Keys.SKIP_GCODE_COMMANDS).split("\n")
]
)
while result[0] is not None:
if type(result[0]) != str:
return result
line = result[0].strip()
if line == "":
return result

# Normalize command, without uppercase
cmd = result[0].split(";", 1)[0].strip().upper()
if cmd not in cmd_list:
break
self._logger.warning(f"Skip GCODE: {result}")
result = self._jobCommReaderOrig(*args, **kwargs)
except Exception:
self._logger.error(traceback.format_exc())
finally:
return result

def _init_fileshare(self, fs_cls=Fileshare):
# Note: fileshare_dir referenced when cleaning up old files
self.fileshare_dir = self._path_on_disk(
Expand Down
41 changes: 41 additions & 0 deletions continuousprint/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,47 @@ def testSpoolManagerFound(self):
self.assertEqual(p._get_key(Keys.MATERIAL_SELECTION), True) # Spoolmanager
self.assertEqual(p._get_key(Keys.RESTART_ON_PAUSE), False) # Obico

def testPatchCommJobReader(self):
p = mockplugin()
gnfj = p._printer._comm._get_next_from_job
p.d = MagicMock(_state_printing="foo", state="foo")
p._set_key(Keys.SKIP_GCODE_COMMANDS, "FOO 1\nBAR ; Settings comment")
p.patchCommJobReader()

mm = MagicMock()
gnfj.side_effect = [
(line, None, None)
for line in (
"",
mm,
"foo 1", # Case insensitive
"BAR ; I have a comment that should be ignored ;;;",
"G0 X0",
None,
)
]

# Passes whitespace lines
self.assertEqual(p._printer._comm._get_next_from_job(), ("", ANY, ANY))

# Passes foreign objects (e.g. for SendQueueMarker in OctoPrint)
self.assertEqual(p._printer._comm._get_next_from_job(), (mm, ANY, ANY))

# Skips cmds in skip-list
self.assertEqual(p._printer._comm._get_next_from_job(), ("G0 X0", ANY, ANY))

# Stops on end of file
self.assertEqual(p._printer._comm._get_next_from_job(), (None, ANY, ANY))

# Test exception inside loop returns a decent result
gnfj.side_effect = [("foo 1", None, None), Exception("Testing exception")]
self.assertEqual(p._printer._comm._get_next_from_job(), ("foo 1", ANY, ANY))

# Ignored when not printing
p.d = MagicMock(_state_printing="foo", state="bar")
gnfj.side_effect = [("foo 1", None, None)]
self.assertEqual(p._printer._comm._get_next_from_job(), ("foo 1", ANY, ANY))

def testDBNew(self):
p = mockplugin()
with tempfile.TemporaryDirectory() as td:
Expand Down
6 changes: 6 additions & 0 deletions continuousprint/templates/continuousprint_settings.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,12 @@
<input type=checkbox data-bind="checked: settings.settings.plugins.continuousprint.cp_auto_reconnect">
</div>
</div>
<div class="control-group" title="Put a newline-separated list of GCODE commands to ignore while the queue is running, e.g. 'M104 S0' to prevent gcode files from turning off the hotend. CPQ event scripts/preprocessors are unaffected. Requires a restart of OctoPrint when set for the first time.">
<label class="control-label">Ignore GCODE lines while printing</label>
<div class="controls">
<textarea rows="4" data-bind="value: settings.settings.plugins.continuousprint.cp_skip_gcode_commands"></textarea>
</div>
</div>
</fieldset>

<legend>Bed Cooldown Settings</legend>
Expand Down
41 changes: 41 additions & 0 deletions docs/gcode-scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,47 @@ Now try it out! Whenever your event fires, it should run this new script.

You can also run multiple scripts in the same event - they are executed from top to bottom, and you can drag to reorder them.

### Advanced: Skip certain commands in GCODE files

In `Settings > Continuous Print > Behaviors` there's an `Ignore GCODE lines while printing` text box where you can specify parts of `.gcode` files to ignore.

This is useful if your gcode slicer includes cleanup code to turn off hotends, heated beds, stepper motors etc. which may be important when printing without Continuous Print, but which aren't useful when printing multiple files in a row.

Here's an example of the end of a .gcode file sliced with Kiri:Moto:

```
; --- shutdown ---
G28 ;(Stick out the part)
M190 S0 ;(Turn off heat bed, don't wait.)
G92 E10 ;(Set extruder to 10)
G1 E7 F200 ;(retract 3mm)
M104 S0 ;(Turn off nozzle, don't wait)
M107 ;(Turn off part fan)
M84 ;(Turn off stepper motors.)
M82 ; absolute extrusion mode
M104 T0 S0
```

In this case, you may want to have the following configuration for `Ignore GCODE lines while printing`:

```
M190 S0
M104 S0
M104 T0 S0
M107 ; may also occur at the start, bed adhesion may be affected
M84
```

Be advised that:

* This setting does not apply to Continuous Print event scripts, only to `.gcode` files being printed.
* Comments are stripped away when comparing lines in the .gcode file to lines in the settings textbox
(e.g. `M84 ; this is a comment` will match `M84 ; a different comment`)
* Commands are not case sensitive (e.g. `m84` will match `M84`)
* Apart from the above behaviors, the commands must match exactly (e.g. `M190` in gcode will not match `M190 S0`)
* Any removed cooling / stepper disabling commands should probably be added to a "Queue Finished" event script, so that the printer is put in a safe state
when all jobs are completed.

### Optional: Use BedReady to check bed state

[OctoPrint-BedReady](https://plugins.octoprint.org/plugins/bedready/) is a plugin that checks the webcam image of the bed against a refrence image where the bed is clear.
Expand Down

0 comments on commit 4950f39

Please sign in to comment.