From 261d6bbc6b8cacb42abaf3c6cfeb8ac2ae820ae9 Mon Sep 17 00:00:00 2001 From: Scott Martin Date: Tue, 15 Nov 2022 21:09:02 -0500 Subject: [PATCH 1/4] Add asteval based conditional execution, partially implement integration tests --- continuousprint/integration_test.py | 53 +++++++++++++++++++++++++ continuousprint/script_runner.py | 13 +++++- continuousprint/script_runner_test.py | 3 ++ continuousprint/storage/queries.py | 10 +++-- continuousprint/storage/queries_test.py | 17 ++++++-- setup.py | 2 +- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/continuousprint/integration_test.py b/continuousprint/integration_test.py index 6bcf028..cc555c8 100644 --- a/continuousprint/integration_test.py +++ b/continuousprint/integration_test.py @@ -13,6 +13,7 @@ from .queues.lan import LANQueue from .queues.abstract import Strategy from .data import CustomEvents +from .script_runner import ScriptRunner from peewee import SqliteDatabase from collections import defaultdict from peerprint.lan_queue import LANPrintQueueBase @@ -175,6 +176,58 @@ def test_completes_job_in_order(self): self.assert_from_printing_state("b.gcode", finishing=True) +class TestDriver(DBTest): + def setUp(self): + super().setUp() + + def onupdate(): + pass + + self.s = ScriptRunner( + msg=MagicMock(), + file_manager=MagicMock(), + logger=logging.getLogger(), + printer=MagicMock(), + refresh_ui_state=MagicMock(), + fire_event=MagicMock(), + ) + self.s._get_user = lambda: "foo" + self.s._wrap_stream = MagicMock(return_value=None) + self.mq = MultiQueue(queries, Strategy.IN_ORDER, onupdate) + self.d = Driver( + queue=self.mq, + script_runner=self.s, + logger=logging.getLogger(), + ) + self.d.set_retry_on_pause(True) + self.d.action(DA.DEACTIVATE, DP.IDLE) + + def _setup_condition(self, cond): + queries.assignScriptsAndEvents( + dict(foo="G0 X20"), {CustomEvents.ACTIVATE.event: [("foo", cond)]} + ) + self.d.action(DA.ACTIVATE, DP.IDLE) + + def test_conditional_true(self): + self._setup_condition("2 + 2 == 4") + self.assertEqual(self.d.state.__name__, self.d._state_activating.__name__) + + def test_conditional_false(self): + self._setup_condition("2 + 2 == 5") + self.assertEqual(self.d.state.__name__, self.d._state_idle.__name__) + + def test_conditional_error(self): + self._setup_condition("1 / 0") + self.assertEqual(self.d.state.__name__, self.d._state_idle.__name__) + + def test_conditional_print_volume(self): + raise NotImplementedError + self._setup_condition("1 / 0") + # TODO pass printing file name to ScriptRunner and use its filemanager + # to get_additional_metadata when setting up the interpreter + self.assertEqual(self.d.state.__name__, self.d._state_idle.__name__) + + class LocalLockManager: def __init__(self, locks, ns): self.locks = locks diff --git a/continuousprint/script_runner.py b/continuousprint/script_runner.py index 2c2903d..7cd33ad 100644 --- a/continuousprint/script_runner.py +++ b/continuousprint/script_runner.py @@ -1,6 +1,7 @@ import time from io import BytesIO +from asteval import Interpreter from pathlib import Path from octoprint.filemanager.util import StreamWrapper from octoprint.filemanager.destinations import FileDestinations @@ -27,6 +28,7 @@ def __init__( self._printer = printer self._refresh_ui_state = refresh_ui_state self._fire_event = fire_event + self._symbols = dict() def _get_user(self): try: @@ -66,8 +68,17 @@ def _do_msg(self, evt, running=False): elif evt == CustomEvents.AWAITING_MATERIAL: self._msg("Running script while awaiting material") + def update_interpreter_symbols(self, symbols): + self._symbols = symbols + + def _get_interpreter(self): + interp = Interpreter() + interp.symtable = self._symbols.copy() + return interp + def run_script_for_event(self, evt, msg=None, msgtype=None): - gcode = genEventScript(evt) + gcode = genEventScript(evt, self._get_interpreter()) + self._do_msg(evt, running=gcode != "") # Cancellation happens before custom scripts are run diff --git a/continuousprint/script_runner_test.py b/continuousprint/script_runner_test.py index 8593199..2b32c18 100644 --- a/continuousprint/script_runner_test.py +++ b/continuousprint/script_runner_test.py @@ -5,6 +5,7 @@ from .script_runner import ScriptRunner from .data import CustomEvents from .storage.database_test import DBTest +from .storage.queries import assignScriptsAndEvents import logging logging.basicConfig(level=logging.DEBUG) @@ -26,8 +27,10 @@ def setUp(self): ) self.s._get_user = lambda: "foo" self.s._wrap_stream = MagicMock(return_value=None) + self.s._get_interpreter = lambda: None def test_run_script_for_event(self): + # Note: default scripts are populated on db_init for FINISH and PRINT_SUCCESS self.s.run_script_for_event(CustomEvents.FINISH) self.s._file_manager.add_file.assert_called() self.s._printer.select_file.assert_called_with( diff --git a/continuousprint/storage/queries.py b/continuousprint/storage/queries.py index 4c50e6e..72d242c 100644 --- a/continuousprint/storage/queries.py +++ b/continuousprint/storage/queries.py @@ -458,8 +458,9 @@ def assignScriptsAndEvents(scripts, events): for k, v in scripts.items(): s[k] = Script.create(name=k, body=v) for k, v in events.items(): - for i, n in enumerate(v): - Event.create(name=k, script=s[n], rank=i) + print(k, v) + for i, nn in enumerate(v): + Event.create(name=k, script=s[nn[0]], condition=nn[1], rank=i) def getScriptsAndEvents(): @@ -475,7 +476,7 @@ def getScriptsAndEvents(): return dict(scripts=scripts, events=events) -def genEventScript(evt: CustomEvents) -> str: +def genEventScript(evt: CustomEvents, interp=None) -> str: result = [] for e in ( Event.select() @@ -483,5 +484,6 @@ def genEventScript(evt: CustomEvents) -> str: .where(Event.name == evt.event) .order_by(Event.rank) ): - result.append(e.script.body) + if e.condition is None or (interp is not None and interp(e.condition) is True): + result.append(e.script.body) return "\n".join(result) diff --git a/continuousprint/storage/queries_test.py b/continuousprint/storage/queries_test.py index 2e84f83..457ca73 100644 --- a/continuousprint/storage/queries_test.py +++ b/continuousprint/storage/queries_test.py @@ -360,7 +360,7 @@ def testAnnotateLastRun(self): class TestScriptsAndEvents(DBTest): def testAssignGet(self): - q.assignScriptsAndEvents(dict(foo="bar"), dict(evt=["foo"])) + q.assignScriptsAndEvents(dict(foo="bar"), dict(evt=[("foo", None)])) self.assertEqual( q.getScriptsAndEvents(), dict( @@ -372,12 +372,23 @@ def testAssignGet(self): def testMultiScriptEvent(self): evt = CustomEvents.PRINT_SUCCESS.event q.assignScriptsAndEvents( - dict(s1="gcode1", s2="gcode2"), dict([(evt, ["s1", "s2"])]) + dict(s1="gcode1", s2="gcode2"), dict([(evt, [("s1", None), ("s2", None)])]) ) self.assertEqual(q.genEventScript(CustomEvents.PRINT_SUCCESS), "gcode1\ngcode2") # Ordering of event matters q.assignScriptsAndEvents( - dict(s1="gcode1", s2="gcode2"), dict([(evt, ["s2", "s1"])]) + dict(s1="gcode1", s2="gcode2"), dict([(evt, [("s2", None), ("s1", None)])]) ) self.assertEqual(q.genEventScript(CustomEvents.PRINT_SUCCESS), "gcode2\ngcode1") + + def testConditionalEval(self): + evt = CustomEvents.PRINT_SUCCESS.event + q.assignScriptsAndEvents(dict(s1="gcode1"), dict([(evt, [("s1", "cond")])])) + + self.assertEqual( + q.genEventScript(CustomEvents.PRINT_SUCCESS, lambda cond: True), "gcode1" + ) + self.assertEqual( + q.genEventScript(CustomEvents.PRINT_SUCCESS, lambda cond: False), "" + ) diff --git a/setup.py b/setup.py index 3fc21c3..b8986f6 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = ["peewee<4", "peerprint==0.1.0"] +plugin_requires = ["peewee<4", "peerprint==0.1.0", "asteval==0.9.28"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point From 4b860c803a49129b3426367a397037b863570dbc Mon Sep 17 00:00:00 2001 From: Scott Martin Date: Wed, 16 Nov 2022 10:08:19 -0500 Subject: [PATCH 2/4] Fix tests and clean up --- continuousprint/driver.py | 8 +++++ continuousprint/integration_test.py | 26 +++++++++----- continuousprint/script_runner.py | 13 ++++++- .../static/js/continuousprint_settings.js | 17 ++++++---- .../js/continuousprint_settings.test.js | 6 ++-- continuousprint/storage/queries.py | 23 +++++++++---- continuousprint/storage/queries_test.py | 34 ++++++++++++++++--- .../templates/continuousprint_settings.jinja2 | 2 +- 8 files changed, 99 insertions(+), 30 deletions(-) diff --git a/continuousprint/driver.py b/continuousprint/driver.py index 4e98e1e..fa54384 100644 --- a/continuousprint/driver.py +++ b/continuousprint/driver.py @@ -69,6 +69,7 @@ def __init__( self._update_ui = False self._cur_path = None self._cur_materials = [] + self._bed_temp = 0 def action( self, @@ -103,6 +104,13 @@ def action( self._cur_materials = materials if bed_temp is not None: self._bed_temp = bed_temp + self._runner.update_interpreter_symbols( + dict( + path=self._cur_path, + materials=self._cur_materials, + bed_temp=self._bed_temp, + ) + ) # Deactivation must be allowed on all states, so we hande it here for # completeness. diff --git a/continuousprint/integration_test.py b/continuousprint/integration_test.py index cc555c8..88c7b47 100644 --- a/continuousprint/integration_test.py +++ b/continuousprint/integration_test.py @@ -183,9 +183,10 @@ def setUp(self): def onupdate(): pass + self.fm = MagicMock() self.s = ScriptRunner( msg=MagicMock(), - file_manager=MagicMock(), + file_manager=self.fm, logger=logging.getLogger(), printer=MagicMock(), refresh_ui_state=MagicMock(), @@ -202,11 +203,13 @@ def onupdate(): self.d.set_retry_on_pause(True) self.d.action(DA.DEACTIVATE, DP.IDLE) - def _setup_condition(self, cond): + def _setup_condition(self, cond, path=None): + self.d.state = self.d._state_inactive queries.assignScriptsAndEvents( - dict(foo="G0 X20"), {CustomEvents.ACTIVATE.event: [("foo", cond)]} + dict(foo="G0 X20"), + {CustomEvents.ACTIVATE.event: [dict(script="foo", condition=cond)]}, ) - self.d.action(DA.ACTIVATE, DP.IDLE) + self.d.action(DA.ACTIVATE, DP.IDLE, path=path) def test_conditional_true(self): self._setup_condition("2 + 2 == 4") @@ -221,10 +224,17 @@ def test_conditional_error(self): self.assertEqual(self.d.state.__name__, self.d._state_idle.__name__) def test_conditional_print_volume(self): - raise NotImplementedError - self._setup_condition("1 / 0") - # TODO pass printing file name to ScriptRunner and use its filemanager - # to get_additional_metadata when setting up the interpreter + depthpath = "metadata['analysis']['dimensions']['depth']" + self.fm.file_exists.return_value = True + self.fm.has_analysis.return_value = True + self.fm.get_metadata.return_value = dict( + analysis=dict(dimensions=dict(depth=39)) + ) + + self._setup_condition(f"{depthpath} < 40", path="foo.gcode") + self.assertEqual(self.d.state.__name__, self.d._state_activating.__name__) + + self._setup_condition(f"{depthpath} > 39", path="foo.gcode") self.assertEqual(self.d.state.__name__, self.d._state_idle.__name__) diff --git a/continuousprint/script_runner.py b/continuousprint/script_runner.py index 7cd33ad..e022ca0 100644 --- a/continuousprint/script_runner.py +++ b/continuousprint/script_runner.py @@ -70,6 +70,17 @@ def _do_msg(self, evt, running=False): def update_interpreter_symbols(self, symbols): self._symbols = symbols + path = self._symbols.get("path") + if ( + path is not None + and self._file_manager.file_exists(FileDestinations.LOCAL, path) + and self._file_manager.has_analysis(FileDestinations.LOCAL, path) + ): + # See https://docs.octoprint.org/en/master/modules/filemanager.html#octoprint.filemanager.analysis.GcodeAnalysisQueue + # for analysis values - or `.metadata.json` within .octoprint/uploads + self._symbols["metadata"] = self._file_manager.get_metadata( + FileDestinations.LOCAL, path + ) def _get_interpreter(self): interp = Interpreter() @@ -77,7 +88,7 @@ def _get_interpreter(self): return interp def run_script_for_event(self, evt, msg=None, msgtype=None): - gcode = genEventScript(evt, self._get_interpreter()) + gcode = genEventScript(evt, self._get_interpreter(), self._logger) self._do_msg(evt, running=gcode != "") diff --git a/continuousprint/static/js/continuousprint_settings.js b/continuousprint/static/js/continuousprint_settings.js index 3368318..70f8d5a 100644 --- a/continuousprint/static/js/continuousprint_settings.js +++ b/continuousprint/static/js/continuousprint_settings.js @@ -106,12 +106,16 @@ function CPSettingsViewModel(parameters, profiles=CP_PRINTER_PROFILES, default_s } self.rmScript = function(s) { for (let e of self.events()) { - e.actions.remove(s); + for (let a of e.actions()) { + if (a.name() == s.name()) { + e.actions.remove(a); + } + } } self.scripts.remove(s); } - self.addAction = function(e, a) { - e.actions.push(a); + self.addAction = function(e, s) { + e.actions.push({...s, condition: ko.observable("")}); }; self.rmAction = function(e, a) { e.actions.remove(a); @@ -190,7 +194,7 @@ function CPSettingsViewModel(parameters, profiles=CP_PRINTER_PROFILES, default_s for (let a of result.events[k.event] || []) { for (let s of scripts) { if (s.name() === a.script) { - actions.push({...s, condition: a.condition}); + actions.push({...s, condition: ko.observable(a.condition)}); break; } } @@ -226,11 +230,10 @@ function CPSettingsViewModel(parameters, profiles=CP_PRINTER_PROFILES, default_s scripts[s.name()] = s.body(); } let events = {}; - // TODO push conditions for (let e of self.events()) { let ks = []; - for (let ea of e.actions()) { - ks.push(ea.name()); + for (let a of e.actions()) { + ks.push({script: a.name(), condition: a.condition()}); } if (ks.length !== 0) { events[e.event] = ks; diff --git a/continuousprint/static/js/continuousprint_settings.test.js b/continuousprint/static/js/continuousprint_settings.test.js index 1b80e91..e0eb34c 100644 --- a/continuousprint/static/js/continuousprint_settings.test.js +++ b/continuousprint/static/js/continuousprint_settings.test.js @@ -193,9 +193,9 @@ test('addScript, rmScript', () => { test('addAction, rmAction', () => { let v = new VM.CPSettingsViewModel(mocks(), PROFILES, SCRIPTS, EVENTS); let e = {"actions": ko.observableArray([])}; - let a = "foo"; + let a = {script:"foo"}; v.addAction(e, a); - expect(e.actions()[0]).toEqual(a); - v.rmAction(e, a); + expect(e.actions()[0].script).toEqual(a.script); + v.rmAction(e, e.actions()[0]); expect(e.actions().length).toEqual(0); }); diff --git a/continuousprint/storage/queries.py b/continuousprint/storage/queries.py index 72d242c..8efc3de 100644 --- a/continuousprint/storage/queries.py +++ b/continuousprint/storage/queries.py @@ -457,10 +457,12 @@ def assignScriptsAndEvents(scripts, events): s = dict() for k, v in scripts.items(): s[k] = Script.create(name=k, body=v) - for k, v in events.items(): - print(k, v) - for i, nn in enumerate(v): - Event.create(name=k, script=s[nn[0]], condition=nn[1], rank=i) + for k, e in events.items(): + print(k, e) + for i, a in enumerate(e): + Event.create( + name=k, script=s[a["script"]], condition=a["condition"], rank=i + ) def getScriptsAndEvents(): @@ -476,7 +478,7 @@ def getScriptsAndEvents(): return dict(scripts=scripts, events=events) -def genEventScript(evt: CustomEvents, interp=None) -> str: +def genEventScript(evt: CustomEvents, interp=None, logger=None) -> str: result = [] for e in ( Event.select() @@ -484,6 +486,15 @@ def genEventScript(evt: CustomEvents, interp=None) -> str: .where(Event.name == evt.event) .order_by(Event.rank) ): - if e.condition is None or (interp is not None and interp(e.condition) is True): + should_run = True + if e.condition is not None and e.condition != "": + should_run = interp(e.condition) + if logger: + logger.info( + f"Event condition for script {e.script.name}: {e.condition}\nSymbols: {interp.symtable}\nResult: {should_run}" + ) + if should_run is True: + if logger: + logger.info(f"Appending script {e.script.name}") result.append(e.script.body) return "\n".join(result) diff --git a/continuousprint/storage/queries_test.py b/continuousprint/storage/queries_test.py index 457ca73..cc49874 100644 --- a/continuousprint/storage/queries_test.py +++ b/continuousprint/storage/queries_test.py @@ -360,7 +360,9 @@ def testAnnotateLastRun(self): class TestScriptsAndEvents(DBTest): def testAssignGet(self): - q.assignScriptsAndEvents(dict(foo="bar"), dict(evt=[("foo", None)])) + q.assignScriptsAndEvents( + dict(foo="bar"), dict(evt=[dict(script="foo", condition=None)]) + ) self.assertEqual( q.getScriptsAndEvents(), dict( @@ -372,19 +374,43 @@ def testAssignGet(self): def testMultiScriptEvent(self): evt = CustomEvents.PRINT_SUCCESS.event q.assignScriptsAndEvents( - dict(s1="gcode1", s2="gcode2"), dict([(evt, [("s1", None), ("s2", None)])]) + dict(s1="gcode1", s2="gcode2"), + dict( + [ + ( + evt, + [ + dict(script="s1", condition=None), + dict(script="s2", condition=None), + ], + ) + ] + ), ) self.assertEqual(q.genEventScript(CustomEvents.PRINT_SUCCESS), "gcode1\ngcode2") # Ordering of event matters q.assignScriptsAndEvents( - dict(s1="gcode1", s2="gcode2"), dict([(evt, [("s2", None), ("s1", None)])]) + dict(s1="gcode1", s2="gcode2"), + dict( + [ + ( + evt, + [ + dict(script="s2", condition=None), + dict(script="s1", condition=None), + ], + ) + ] + ), ) self.assertEqual(q.genEventScript(CustomEvents.PRINT_SUCCESS), "gcode2\ngcode1") def testConditionalEval(self): evt = CustomEvents.PRINT_SUCCESS.event - q.assignScriptsAndEvents(dict(s1="gcode1"), dict([(evt, [("s1", "cond")])])) + q.assignScriptsAndEvents( + dict(s1="gcode1"), dict([(evt, [dict(script="s1", condition="cond")])]) + ) self.assertEqual( q.genEventScript(CustomEvents.PRINT_SUCCESS, lambda cond: True), "gcode1" diff --git a/continuousprint/templates/continuousprint_settings.jinja2 b/continuousprint/templates/continuousprint_settings.jinja2 index e163347..d9b8634 100644 --- a/continuousprint/templates/continuousprint_settings.jinja2 +++ b/continuousprint/templates/continuousprint_settings.jinja2 @@ -77,7 +77,7 @@
- +
From 2ee8a026f9dbae725a48d5368bb4bc5bfcd0716c Mon Sep 17 00:00:00 2001 From: Scott Martin Date: Wed, 16 Nov 2022 11:52:42 -0500 Subject: [PATCH 3/4] Update docs on scripting, add info on Conditions --- .../static/js/continuousprint_settings.js | 2 +- docs/gcode-scripting.md | 143 +++++++++++++++++- 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/continuousprint/static/js/continuousprint_settings.js b/continuousprint/static/js/continuousprint_settings.js index 70f8d5a..3e6c82b 100644 --- a/continuousprint/static/js/continuousprint_settings.js +++ b/continuousprint/static/js/continuousprint_settings.js @@ -101,7 +101,7 @@ function CPSettingsViewModel(parameters, profiles=CP_PRINTER_PROFILES, default_s self.scripts.push({ name: ko.observable(""), body: ko.observable(""), - expanded: ko.observable(true), + expanded: ko.observable(false), }); } self.rmScript = function(s) { diff --git a/docs/gcode-scripting.md b/docs/gcode-scripting.md index 55b06cd..b6a5946 100644 --- a/docs/gcode-scripting.md +++ b/docs/gcode-scripting.md @@ -1,12 +1,59 @@ -# GCODE Scripting +# Events and Scripting -GCODE scripts can be quite complex - if you want to learn the basics, try reading through [this primer](https://www.simplify3d.com/support/articles/3d-printing-gcode-tutorial/). +**Control what happens in between queued prints by using Events to run Scripts**. -## Bed clearing script +## Events -When Continuous Print is managing the queue, this script is run after every print completes - **including prints started before queue managing begins**. +Events fire at certain points when the queue is being run - when a print completes, for instance. You can see the full list of events by going to `Settings > Continuous Print > Events` or [looking here in the source code](https://github.com/smartin015/continuousprint/blob/master/continuousprint/data/__init__.py). -Your cleaning script should remove all 3D print material from the build area to make way for the next print. +When an event fires, you can run one or more configured scripts conditionally (see [Conditions](#conditions) below). These events are visible to other OctoPrint plugins (e.g. [OctoPrint-IFTTT](https://plugins.octoprint.org/plugins/IFTTT/)) and can be used to perform actions in them. + +## Scripts + +Event scripts, just like 3D print files, are in GCODE. It's a series of commands that are sent to the printer that tell it to move, heat up, cool down, etc. + +GCODE scripts can be quite complex - if you want to learn the basics, try reading through [this primer](https://www.simplify3d.com/support/articles/3d-printing-gcode-tutorial/). Example scripts are also provided below. + +## Load Defaults + +If your 3D printer is common, you should first check the user-contributed default scripts for your printer. + +To load default scripts: + +1. Navigate to `Settings > Continuous Print > Profile` and ensure the make and model of your 3D printer is correct. +1. Click on the `Scripts` tab, then click `Load from Profile`. You should see two new scripts ("Bed Clearing" and "Finished") appear matching your printer make and model. +1. Click on the `Events` tab and scroll down to `Print Success`. +1. Replace the Bed Clearing script there with the new one. +1. Scroll down to `Queue Finished` and replace its script with the new script. +1. Click `Save`. + +If you want to contribute a change or a new default script, read the [Contributing](#contributing) section below. + +## Custom Scripts + +You can also set up your own custom scripts to run when events happen. + +To add a new custom script: + +1. Navigate to `Settings > Continuous Print > Scripts`, then click `New Script`. A new (unnamed) script will appear in the list. +1. Put your GCODE script in the large text box - as an example, try typing in `@pause` to pause the print and wait for you to resume it. +1. Give the script a name (e.g. "Example Script"), then click the Done button at the bottom of the edit area. + +Your script is now created, but it will not run until we assign it to one or more events. + +To register the script to an event: + +1. Click the `Events` tab and scroll to the desired event. For example, `Queue Deactivated` which runs when you click the `Stop Managing` button. +1. Click the `Add Script` button, then the name of your script. You should now see it listed below the event name. +1. Click `Save` to save your settings. + +Now try it out! Whenever your event fires, it should run this new script. + +!!! Tip + + You can use the same script for multiple events, e.g. run bed clearing after each print *and* when the last print is finished. + + You can also run multiple scripts in the same event - they are executed from top to bottom, and you can drag to reorder them. ### Optional: Use BedReady to check bed state @@ -14,9 +61,91 @@ Your cleaning script should remove all 3D print material from the build area to If you install BedReady, you can add an automated check that the bed is clear for the next print by adding `@BEDREADY` onto the end of your bed clearing script. -## Queue finished script +## Conditions + +You may discover that you only want your event scripts to run sometimes - maybe your printer can only clear prints of a certain size, or you want to sweep prints off in a different direction depending on their material or file name. + +This can be done by adding a **Condition**, which are little bits of code that return either `True` or `False` to indicate whether or not to run your script. + +Conditions are configured per assigned script in the `Events` settings tab, and evaluate based on instantaneous state details as well as analysis metadata about the print. + +### Language + +Conditions are evaluated using [ASTEVAL](https://newville.github.io/asteval/) which is a [Python](https://www.python.org/)-like interpreter. Most simple Python scripts will run just fine. + +However, **every condition must have a boolean expression as its final line of code**. This final expression determines whether or not we should run the script. + +If you're new to writing Python code and the [examples below](#example-conditions) don't have the answers you need, check out [here](https://wiki.python.org/moin/BeginnersGuide) for language resources. + +### Available State + +When you write a Condition, you will reference external information in your expression in order to return a boolean result. This is done by accessing `State` variables (referred to in [ASTEVAL docs](https://newville.github.io/asteval/) as the "symbol table"). + +Here's an example of what you can expect for state variables: + +``` +path: 'testprint.gcode', +materials: ['PLA_red_#ff0000'], +bed_temp: 23.59, +metadata: { + 'hash': '38eea2d4463053bd79af52c3fadc37deaa7bfff7', + 'analysis': { + 'printingArea': {'maxX': 5.3, 'maxY': 7.65, 'maxZ': 19.7, 'minX': -5.3, 'minY': -8.5608, 'minZ': 0.0}, + 'dimensions': {'depth': 16.2108, 'height': 19.7, 'width': 10.6}, + 'estimatedPrintTime': 713.6694555778557, + 'filament': {'tool0': {'length': 311.02239999999875, 'volume': 0.0}} + }, + 'continuousprint': { + 'profile': 'Monoprice Mini Delta V2' + }, + 'history': [ + { + 'timestamp': 1660053581.8503253, + 'printTime': 109.47731690102955, + 'success': True, + 'printerProfile': '_default' + }, + ], + 'statistics': { + 'averagePrintTime': {'_default': 113.51082421375965}, + 'lastPrintTime': {'_default': 306.7005050050211} + } +} +``` + +Note that `path`, `materials`, and `bed_temp` are all instantaneous variables about the current state, while `metadata` comes from file metadata analysis and is absent if `path` is None or empty. + +See also `update_interpreter_symbols` in [driver.py](https://github.com/smartin015/continuousprint/blob/master/continuousprint/driver.py) for how state is constructed and sent to the interpreter. + +### Example Conditions + +Here's a few examples you can use as conditions for running your scripts. Just copy and paste into the text box next to your script in the `Events` settings tab, then click `Save` to apply them. + +Note that in all cases, the last line of the condition evalutes to a boolean value and is not assigned to a variable - this is what we use to determine whether to run or skip the event script. + +**Run script if the bed is hot** + +`bed_temp > 40` + +**Run script if this print's filename ends in "\_special.gcode"** + +`path.endswith("_special.gcode")` + +**Run script if this print will be at least 10mm high** + +`metadata["analysis"]["dimensions"]["height"] >= 10` + +**Run script if this print takes on average over an hour to complete** + +`metadata["statistics"]["averagePrintTime"]["_default"] > 60*60` + +**Run script if this print has failed more than 10% of the time** -This script is run after all prints in the queue have been printed. Use this script to put the machine in a safe resting state. Note that the last print will have already been cleared by the bed cleaning script (above). +``` +# Div by 1 when history is empty to prevent divide by zero +failure_ratio = len([h for h in metadata["history"] if not h['success']]) / max(1, len(metadata["history"])) +False if len(metadata["history"]) == 0 else failure_ratio > 0.1 +``` ## Contributing From 16e1db150f87d5313c76e57c467ab04b0afb01e7 Mon Sep 17 00:00:00 2001 From: Scott Martin Date: Wed, 16 Nov 2022 11:54:51 -0500 Subject: [PATCH 4/4] Fix file import icon --- continuousprint/templates/continuousprint_settings.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/continuousprint/templates/continuousprint_settings.jinja2 b/continuousprint/templates/continuousprint_settings.jinja2 index d9b8634..604388e 100644 --- a/continuousprint/templates/continuousprint_settings.jinja2 +++ b/continuousprint/templates/continuousprint_settings.jinja2 @@ -174,7 +174,7 @@