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 6bcf028..88c7b47 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,68 @@ 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.fm = MagicMock() + self.s = ScriptRunner( + msg=MagicMock(), + file_manager=self.fm, + 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, path=None): + self.d.state = self.d._state_inactive + queries.assignScriptsAndEvents( + dict(foo="G0 X20"), + {CustomEvents.ACTIVATE.event: [dict(script="foo", condition=cond)]}, + ) + self.d.action(DA.ACTIVATE, DP.IDLE, path=path) + + 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): + 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__) + + class LocalLockManager: def __init__(self, locks, ns): self.locks = locks diff --git a/continuousprint/script_runner.py b/continuousprint/script_runner.py index 2c2903d..e022ca0 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,28 @@ 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 + 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() + 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._logger) + 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/static/js/continuousprint_settings.js b/continuousprint/static/js/continuousprint_settings.js index 3368318..3e6c82b 100644 --- a/continuousprint/static/js/continuousprint_settings.js +++ b/continuousprint/static/js/continuousprint_settings.js @@ -101,17 +101,21 @@ 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) { 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 4c50e6e..8efc3de 100644 --- a/continuousprint/storage/queries.py +++ b/continuousprint/storage/queries.py @@ -457,9 +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(): - for i, n in enumerate(v): - Event.create(name=k, script=s[n], 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(): @@ -475,7 +478,7 @@ def getScriptsAndEvents(): return dict(scripts=scripts, events=events) -def genEventScript(evt: CustomEvents) -> str: +def genEventScript(evt: CustomEvents, interp=None, logger=None) -> str: result = [] for e in ( Event.select() @@ -483,5 +486,15 @@ def genEventScript(evt: CustomEvents) -> str: .where(Event.name == evt.event) .order_by(Event.rank) ): - result.append(e.script.body) + 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 2e84f83..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"])) + q.assignScriptsAndEvents( + dict(foo="bar"), dict(evt=[dict(script="foo", condition=None)]) + ) self.assertEqual( q.getScriptsAndEvents(), dict( @@ -372,12 +374,47 @@ 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, + [ + 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", "s1"])]) + 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, [dict(script="s1", condition="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/continuousprint/templates/continuousprint_settings.jinja2 b/continuousprint/templates/continuousprint_settings.jinja2 index e163347..604388e 100644 --- a/continuousprint/templates/continuousprint_settings.jinja2 +++ b/continuousprint/templates/continuousprint_settings.jinja2 @@ -77,7 +77,7 @@
- +
@@ -174,7 +174,7 @@