Skip to content

Commit

Permalink
Merge pull request #50 from smartin015/state_machine
Browse files Browse the repository at this point in the history
Driver uses state machine
  • Loading branch information
smartin015 authored Apr 17, 2022
2 parents bd96945 + 4534402 commit 4786679
Show file tree
Hide file tree
Showing 3 changed files with 434 additions and 342 deletions.
106 changes: 50 additions & 56 deletions continuousprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import octoprint.filemanager
from octoprint.filemanager.util import StreamWrapper
from octoprint.filemanager.destinations import FileDestinations
from octoprint.util import RepeatedTimer


from .print_queue import PrintQueue, QueueItem
from .driver import ContinuousPrintDriver
from .driver import ContinuousPrintDriver, Action as DA, Printer as DP

QUEUE_KEY = "cp_queue"
CLEARING_SCRIPT_KEY = "cp_bed_clearing_script"
Expand Down Expand Up @@ -82,6 +84,10 @@ def get_settings_defaults(self):
d[BED_COOLDOWN_TIMEOUT_KEY] = 60
return d

def _active(self):
return self.d.state != self.d._state_inactive if hasattr(self, "d") else False


def _rm_temp_files(self):
# Clean up any file references from prior runs
for path in TEMP_FILES.values():
Expand All @@ -108,25 +114,40 @@ def on_after_startup(self):
self.q = PrintQueue(self._settings, QUEUE_KEY)
self.d = ContinuousPrintDriver(
queue=self.q,
finish_script_fn=self.run_finish_script,
clear_bed_fn=self.clear_bed,
start_print_fn=self.start_print,
cancel_print_fn=self.cancel_print,
script_runner=self,
logger=self._logger,
)
self.update(DA.DEACTIVATE) # Initializes and passes printer state
self._update_driver_settings()
self._rm_temp_files()
self.next_pause_is_spaghetti = False

# It's possible to miss events or for some weirdness to occur in conditionals. Adding a watchdog
# timer with a periodic tick ensures that the driver knows what the state of the printer is.
self.watchdog = RepeatedTimer(5.0, lambda: self.update(DA.TICK))
self.watchdog.start()
self._logger.info("Continuous Print Plugin started")

def update(self, a: DA):
# Access current file via `get_current_job` instead of `is_current_file` because the latter may go away soon
# See https://docs.octoprint.org/en/master/modules/printer.html#octoprint.printer.PrinterInterface.is_current_file
# Avoid using payload.get('path') as some events may not express path info.
path = self._printer.get_current_job().get("file", {}).get("name")
pstate = self._printer.get_state_id()
p = DP.BUSY
if pstate == "OPERATIONAL":
p = DP.IDLE
elif pstate == "PAUSED":
p = DP.PAUSED

if self.d.action(a, p, path):
self._msg(type="reload") # Reload UI when new state is added

# part of EventHandlerPlugin
def on_event(self, event, payload):
if not hasattr(self, "d"): # Ignore any messages arriving before init
return

# Access current file via `get_current_job` instead of `is_current_file` because the latter may go away soon
# See https://docs.octoprint.org/en/master/modules/printer.html#octoprint.printer.PrinterInterface.is_current_file
# Avoid using payload.get('path') as some events may not express path info.
current_file = self._printer.get_current_job().get("file", {}).get("name")
is_current_path = current_file == self.d.current_path()
is_finish_script = current_file == TEMP_FILES[FINISHED_SCRIPT_KEY]
Expand All @@ -146,64 +167,38 @@ def on_event(self, event, payload):
if self._printer.is_current_file(path, sd=False):
return
self._rm_temp_files()
elif (is_current_path or is_finish_script) and event == Events.PRINT_DONE:
self.d.on_print_success(is_finish_script)
self.paused = False
self._msg(type="reload") # reload UI
elif (
is_current_path
and event == Events.PRINT_FAILED
and payload["reason"] != "cancelled"
):
elif event == Events.PRINT_DONE:
self.update(DA.SUCCESS)
elif event == Events.PRINT_FAILED:
# Note that cancelled events are already handled directly with Events.PRINT_CANCELLED
self.d.on_print_failed()
self.paused = False
self._msg(type="reload") # reload UI
elif is_current_path and event == Events.PRINT_CANCELLED:
self.d.on_print_cancelled(initiator=payload.get('user', None))
self.paused = False
self._msg(type="reload") # reload UI
self.update(DA.FAILURE)
elif event == Events.PRINT_CANCELLED:
print(payload.get('user'))
if payload.get('user') is not None:
self.update(DA.DEACTIVATE)
else:
self.update(DA.TICK)
elif (
is_current_path
and tsd_command is not None
and event == tsd_command
and payload.get("cmd") == "pause"
and payload.get("initiator") == "system"
):
self._logger.info(
"Got spaghetti detection event; flagging next pause event for restart"
)
self.next_pause_is_spaghetti = True
self.update(DA.SPAGHETTI)
elif is_current_path and event == Events.PRINT_PAUSED:
self.d.on_print_paused(
is_temp_file=(payload["path"] in TEMP_FILES.values()),
is_spaghetti=self.next_pause_is_spaghetti,
)
self.next_pause_is_spaghetti = False
self.paused = True
self._msg(type="reload") # reload UI
self.update(DA.TICK)
elif is_current_path and event == Events.PRINT_RESUMED:
self.d.on_print_resumed()
self.paused = False
self._msg(type="reload")
self.update(DA.TICK)
elif (
event == Events.PRINTER_STATE_CHANGED
and self._printer.get_state_id() == "OPERATIONAL"
):
self._msg(type="reload") # reload UI
self.update(DA.TICK)
elif event == Events.UPDATED_FILES:
self._msg(type="updatefiles")
elif event == Events.SETTINGS_UPDATED:
self._update_driver_settings()
# Play out actions until printer no longer in a state where we can run commands
# Note that PAUSED state is respected so that gcode can include `@pause` commands.
# See https://docs.octoprint.org/en/master/features/atcommands.html
while (
self._printer.get_state_id() == "OPERATIONAL"
and self.d.pending_actions() > 0
):
self._logger.warning("on_printer_ready")
self.d.on_printer_ready()

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

def cancel_print(self):
self._msg("Print cancelled", type="error")
Expand Down Expand Up @@ -252,6 +248,7 @@ def clear_bed(self):
self.wait_for_bed_cooldown()
path = self._write_temp_gcode(CLEARING_SCRIPT_KEY)
self._printer.select_file(path, sd=False, printAfterSelect=True)
return path

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

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

# Listen for resume from printer ("M118 //action:queuego"), only act if actually paused. #from @grtrenchman
# Listen for resume from printer ("M118 //action:queuego") #from @grtrenchman
def resume_action_handler(self, comm, line, action, *args, **kwargs):
if not action == "queuego":
return
if self.paused:
self.d.set_active()
self.update(DA.ACTIVATE)

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

# PRIVATE API method - may change without warning.
Expand Down
Loading

0 comments on commit 4786679

Please sign in to comment.