Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional event script execution #150

Merged
merged 4 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions continuousprint/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(
self._update_ui = False
self._cur_path = None
self._cur_materials = []
self._bed_temp = 0

def action(
self,
Expand Down Expand Up @@ -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.
Expand Down
63 changes: 63 additions & 0 deletions continuousprint/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion continuousprint/script_runner.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions continuousprint/script_runner_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand Down
19 changes: 11 additions & 8 deletions continuousprint/static/js/continuousprint_settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions continuousprint/static/js/continuousprint_settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
23 changes: 18 additions & 5 deletions continuousprint/storage/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -475,13 +478,23 @@ 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()
.join(Script, JOIN.LEFT_OUTER)
.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)
43 changes: 40 additions & 3 deletions continuousprint/storage/queries_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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), ""
)
4 changes: 2 additions & 2 deletions continuousprint/templates/continuousprint_settings.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<div class="accordion-group header-row">
<i class="fas fa-grip-vertical"></i>
<div data-bind="text: name" style="flex:1"></div>
<!--<div data-bind="text: condition" style="flex:1"></div>-->
<textarea rows="4" class="input-block-level" data-bind="value: condition"></textarea>
<div style="width: 30px" data-bind="click: (v) => $root.rmAction($parent, v)"><i class="far fa-trash-alt"></i></div>
</div>
</div> <!-- foreach actions -->
Expand Down Expand Up @@ -174,7 +174,7 @@


<button class="btn pull-right" data-bind="click: loadScriptsFromProfile" title="Load user-contributed scripts based on your printer profile">
<i style="cursor:pointer" class="far fa-file-import"></i>&nbsp;Load from Profile
<i style="cursor:pointer" class="fa fa-file-import"></i>&nbsp;Load from Profile
</button>

<button class="btn pull-right" data-bind="click: newScript" title="Create a new script">
Expand Down
Loading