Skip to content

Commit 4786679

Browse files
authored
Merge pull request #50 from smartin015/state_machine
Driver uses state machine
2 parents bd96945 + 4534402 commit 4786679

File tree

3 files changed

+434
-342
lines changed

3 files changed

+434
-342
lines changed

continuousprint/__init__.py

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
import octoprint.filemanager
1616
from octoprint.filemanager.util import StreamWrapper
1717
from octoprint.filemanager.destinations import FileDestinations
18+
from octoprint.util import RepeatedTimer
19+
1820

1921
from .print_queue import PrintQueue, QueueItem
20-
from .driver import ContinuousPrintDriver
22+
from .driver import ContinuousPrintDriver, Action as DA, Printer as DP
2123

2224
QUEUE_KEY = "cp_queue"
2325
CLEARING_SCRIPT_KEY = "cp_bed_clearing_script"
@@ -82,6 +84,10 @@ def get_settings_defaults(self):
8284
d[BED_COOLDOWN_TIMEOUT_KEY] = 60
8385
return d
8486

87+
def _active(self):
88+
return self.d.state != self.d._state_inactive if hasattr(self, "d") else False
89+
90+
8591
def _rm_temp_files(self):
8692
# Clean up any file references from prior runs
8793
for path in TEMP_FILES.values():
@@ -108,25 +114,40 @@ def on_after_startup(self):
108114
self.q = PrintQueue(self._settings, QUEUE_KEY)
109115
self.d = ContinuousPrintDriver(
110116
queue=self.q,
111-
finish_script_fn=self.run_finish_script,
112-
clear_bed_fn=self.clear_bed,
113-
start_print_fn=self.start_print,
114-
cancel_print_fn=self.cancel_print,
117+
script_runner=self,
115118
logger=self._logger,
116119
)
120+
self.update(DA.DEACTIVATE) # Initializes and passes printer state
117121
self._update_driver_settings()
118122
self._rm_temp_files()
119123
self.next_pause_is_spaghetti = False
124+
125+
# It's possible to miss events or for some weirdness to occur in conditionals. Adding a watchdog
126+
# timer with a periodic tick ensures that the driver knows what the state of the printer is.
127+
self.watchdog = RepeatedTimer(5.0, lambda: self.update(DA.TICK))
128+
self.watchdog.start()
120129
self._logger.info("Continuous Print Plugin started")
121130

131+
def update(self, a: DA):
132+
# Access current file via `get_current_job` instead of `is_current_file` because the latter may go away soon
133+
# See https://docs.octoprint.org/en/master/modules/printer.html#octoprint.printer.PrinterInterface.is_current_file
134+
# Avoid using payload.get('path') as some events may not express path info.
135+
path = self._printer.get_current_job().get("file", {}).get("name")
136+
pstate = self._printer.get_state_id()
137+
p = DP.BUSY
138+
if pstate == "OPERATIONAL":
139+
p = DP.IDLE
140+
elif pstate == "PAUSED":
141+
p = DP.PAUSED
142+
143+
if self.d.action(a, p, path):
144+
self._msg(type="reload") # Reload UI when new state is added
145+
122146
# part of EventHandlerPlugin
123147
def on_event(self, event, payload):
124148
if not hasattr(self, "d"): # Ignore any messages arriving before init
125149
return
126150

127-
# Access current file via `get_current_job` instead of `is_current_file` because the latter may go away soon
128-
# See https://docs.octoprint.org/en/master/modules/printer.html#octoprint.printer.PrinterInterface.is_current_file
129-
# Avoid using payload.get('path') as some events may not express path info.
130151
current_file = self._printer.get_current_job().get("file", {}).get("name")
131152
is_current_path = current_file == self.d.current_path()
132153
is_finish_script = current_file == TEMP_FILES[FINISHED_SCRIPT_KEY]
@@ -146,64 +167,38 @@ def on_event(self, event, payload):
146167
if self._printer.is_current_file(path, sd=False):
147168
return
148169
self._rm_temp_files()
149-
elif (is_current_path or is_finish_script) and event == Events.PRINT_DONE:
150-
self.d.on_print_success(is_finish_script)
151-
self.paused = False
152-
self._msg(type="reload") # reload UI
153-
elif (
154-
is_current_path
155-
and event == Events.PRINT_FAILED
156-
and payload["reason"] != "cancelled"
157-
):
170+
elif event == Events.PRINT_DONE:
171+
self.update(DA.SUCCESS)
172+
elif event == Events.PRINT_FAILED:
158173
# Note that cancelled events are already handled directly with Events.PRINT_CANCELLED
159-
self.d.on_print_failed()
160-
self.paused = False
161-
self._msg(type="reload") # reload UI
162-
elif is_current_path and event == Events.PRINT_CANCELLED:
163-
self.d.on_print_cancelled(initiator=payload.get('user', None))
164-
self.paused = False
165-
self._msg(type="reload") # reload UI
174+
self.update(DA.FAILURE)
175+
elif event == Events.PRINT_CANCELLED:
176+
print(payload.get('user'))
177+
if payload.get('user') is not None:
178+
self.update(DA.DEACTIVATE)
179+
else:
180+
self.update(DA.TICK)
166181
elif (
167182
is_current_path
168183
and tsd_command is not None
169184
and event == tsd_command
170185
and payload.get("cmd") == "pause"
171186
and payload.get("initiator") == "system"
172187
):
173-
self._logger.info(
174-
"Got spaghetti detection event; flagging next pause event for restart"
175-
)
176-
self.next_pause_is_spaghetti = True
188+
self.update(DA.SPAGHETTI)
177189
elif is_current_path and event == Events.PRINT_PAUSED:
178-
self.d.on_print_paused(
179-
is_temp_file=(payload["path"] in TEMP_FILES.values()),
180-
is_spaghetti=self.next_pause_is_spaghetti,
181-
)
182-
self.next_pause_is_spaghetti = False
183-
self.paused = True
184-
self._msg(type="reload") # reload UI
190+
self.update(DA.TICK)
185191
elif is_current_path and event == Events.PRINT_RESUMED:
186-
self.d.on_print_resumed()
187-
self.paused = False
188-
self._msg(type="reload")
192+
self.update(DA.TICK)
189193
elif (
190194
event == Events.PRINTER_STATE_CHANGED
191195
and self._printer.get_state_id() == "OPERATIONAL"
192196
):
193-
self._msg(type="reload") # reload UI
197+
self.update(DA.TICK)
194198
elif event == Events.UPDATED_FILES:
195199
self._msg(type="updatefiles")
196200
elif event == Events.SETTINGS_UPDATED:
197201
self._update_driver_settings()
198-
# Play out actions until printer no longer in a state where we can run commands
199-
# Note that PAUSED state is respected so that gcode can include `@pause` commands.
200-
# See https://docs.octoprint.org/en/master/features/atcommands.html
201-
while (
202-
self._printer.get_state_id() == "OPERATIONAL"
203-
and self.d.pending_actions() > 0
204-
):
205-
self._logger.warning("on_printer_ready")
206-
self.d.on_printer_ready()
207202

208203
def _write_temp_gcode(self, key):
209204
gcode = self._settings.get([key])
@@ -221,6 +216,7 @@ def run_finish_script(self):
221216
self._msg("Print Queue Complete", type="complete")
222217
path = self._write_temp_gcode(FINISHED_SCRIPT_KEY)
223218
self._printer.select_file(path, sd=False, printAfterSelect=True)
219+
return path
224220

225221
def cancel_print(self):
226222
self._msg("Print cancelled", type="error")
@@ -252,6 +248,7 @@ def clear_bed(self):
252248
self.wait_for_bed_cooldown()
253249
path = self._write_temp_gcode(CLEARING_SCRIPT_KEY)
254250
self._printer.select_file(path, sd=False, printAfterSelect=True)
251+
return path
255252

256253
def start_print(self, item, clear_bed=True):
257254
self._msg("Starting print: " + item.name)
@@ -264,6 +261,7 @@ def start_print(self, item, clear_bed=True):
264261
self._msg("File not found: " + item.path, type="error")
265262
except InvalidFileType:
266263
self._msg("File not gcode: " + item.path, type="error")
264+
return item.path
267265

268266
def state_json(self, extra_message=None):
269267
# Values are stored json-serialized, so we need to create a json string and inject them into it
@@ -278,19 +276,18 @@ def state_json(self, extra_message=None):
278276
# IMPORTANT: Non-additive changes to this response string must be released in a MAJOR version bump
279277
# (e.g. 1.4.1 -> 2.0.0).
280278
resp = '{"active": %s, "status": "%s", "queue": %s%s}' % (
281-
"true" if hasattr(self, "d") and self.d.active else "false",
279+
"true" if self._active() else "false",
282280
"Initializing" if not hasattr(self, "d") else self.d.status,
283281
q,
284282
extra_message,
285283
)
286284
return resp
287285

288-
# Listen for resume from printer ("M118 //action:queuego"), only act if actually paused. #from @grtrenchman
286+
# Listen for resume from printer ("M118 //action:queuego") #from @grtrenchman
289287
def resume_action_handler(self, comm, line, action, *args, **kwargs):
290288
if not action == "queuego":
291289
return
292-
if self.paused:
293-
self.d.set_active()
290+
self.update(DA.ACTIVATE)
294291

295292
# Public API method returning the full state of the plugin in JSON format.
296293
# See `state_json()` for return values.
@@ -308,10 +305,7 @@ def set_active(self):
308305
if not Permissions.PLUGIN_CONTINUOUSPRINT_STARTQUEUE.can():
309306
return flask.make_response("Insufficient Rights", 403)
310307
self._logger.info("attempt failed due to insufficient permissions.")
311-
self.d.set_active(
312-
flask.request.form["active"] == "true",
313-
printer_ready=(self._printer.get_state_id() == "OPERATIONAL"),
314-
)
308+
self.update(DA.ACTIVATE if flask.request.form["active"] == "true" else DA.DEACTIVATE)
315309
return self.state_json()
316310

317311
# PRIVATE API method - may change without warning.

0 commit comments

Comments
 (0)