Skip to content

Commit

Permalink
Merge pull request #144 from smartin015/rc
Browse files Browse the repository at this point in the history
v2.2.0
  • Loading branch information
smartin015 committed Nov 11, 2022
2 parents 3cc8306 + c8ed747 commit 53ba737
Show file tree
Hide file tree
Showing 63 changed files with 1,561 additions and 658 deletions.
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
FROM python:3.7

# Installing ffmpeg is needed for working with timelapses - this can be ommitted otherwise
# Installing ffmpeg is needed for working with timelapses - can be ommitted otherwise
RUN apt-get update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*

# IPFS installation for LAN filesharing
RUN wget https://dist.ipfs.tech/kubo/v0.15.0/kubo_v0.15.0_linux-amd64.tar.gz \
&& tar -xvzf kubo_v0.15.0_linux-amd64.tar.gz \
&& cd kubo \
&& bash -c ". ./install.sh" \
&& ipfs --version


RUN adduser oprint
USER oprint

Expand Down
5 changes: 5 additions & 0 deletions continuousprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
PRINTER_PROFILES,
GCODE_SCRIPTS,
Keys,
CustomEvents,
ASSETS,
TEMPLATES,
update_info,
Expand Down Expand Up @@ -58,6 +59,7 @@ def on_startup(self, host=None, port=None):
self._logger,
self._identifier,
self._basefolder,
self._event_bus.fire,
)

def on_after_startup(self):
Expand All @@ -73,6 +75,9 @@ def on_after_startup(self):

# ------------------------ Begin EventHandlerPlugin --------------------

def register_custom_events(*args, **kwargs):
return [CustomEvents.__members__.values()]

def on_event(self, event, payload):
if not hasattr(self, "_plugin"):
return
Expand Down
73 changes: 23 additions & 50 deletions continuousprint/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import Enum
from octoprint.access.permissions import Permissions, ADMIN_GROUP
from octoprint.server.util.flask import restricted_access
from .queues.lan import ValidationError
import flask
import json
from .storage import queries
Expand Down Expand Up @@ -174,58 +175,41 @@ def add_job(self):
self._get_queue(DEFAULT_QUEUE).add_job(data.get("name")).as_dict()
)

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/set/mv", methods=["POST"])
@restricted_access
@cpq_permission(Permission.EDITJOB)
def mv_set(self):
self._get_queue(DEFAULT_QUEUE).mv_set(
int(flask.request.form["id"]),
int(
flask.request.form["after_id"]
), # Move to after this set (-1 for beginning of job)
int(
flask.request.form["dest_job"]
), # Move to this job (null for new job at end)
)
return json.dumps("ok")

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/job/mv", methods=["POST"])
@restricted_access
@cpq_permission(Permission.EDITJOB)
def mv_job(self):
self._get_queue(DEFAULT_QUEUE).mv_job(
int(flask.request.form["id"]),
int(
flask.request.form["after_id"]
), # Move to after this job (-1 for beginning of queue)
)
return json.dumps("ok")
src_id = flask.request.form["id"]
after_id = flask.request.form["after_id"]
if after_id == "": # Treat empty string as 'none' i.e. front of queue
after_id = None
sq = self._get_queue(flask.request.form["src_queue"])
dq = self._get_queue(flask.request.form.get("dest_queue"))

# Transfer into dest queue first
if dq != sq:
try:
new_id = dq.import_job_from_view(sq.get_job_view(src_id))
except ValidationError as e:
return json.dumps(dict(error=str(e)))

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/job/submit", methods=["POST"])
@restricted_access
@cpq_permission(Permission.ADDJOB)
def submit_job(self):
j = queries.getJob(int(flask.request.form["id"]))
# Submit to the queue and remove from its origin
err = self._get_queue(flask.request.form["queue"]).submit_job(j)
if err is None:
self._logger.debug(
self._get_queue(DEFAULT_QUEUE).remove_jobs(job_ids=[j.id])
)
return self._state_json()
else:
return json.dumps(dict(error=str(err)))
print("Imported job from view")
sq.remove_jobs([src_id])
src_id = new_id

# Finally, move the job
dq.mv_job(src_id, after_id)
return json.dumps("OK")

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/job/edit", methods=["POST"])
@restricted_access
@cpq_permission(Permission.EDITJOB)
def edit_job(self):
data = json.loads(flask.request.form.get("json"))
return json.dumps(self._get_queue(DEFAULT_QUEUE).edit_job(data["id"], data))
q = self._get_queue(data["queue"])
return json.dumps(q.edit_job(data["id"], data))

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/job/import", methods=["POST"])
Expand Down Expand Up @@ -270,17 +254,6 @@ def rm_job(self):
)
)

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/set/rm", methods=["POST"])
@restricted_access
@cpq_permission(Permission.EDITJOB)
def rm_set(self):
return json.dumps(
self._get_queue(DEFAULT_QUEUE).rm_multi(
set_ids=flask.request.form.getlist("set_ids[]")
)
)

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/job/reset", methods=["POST"])
@restricted_access
Expand Down
8 changes: 8 additions & 0 deletions continuousprint/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
GCODE_SCRIPTS = dict((d["name"], d) for d in yaml.safe_load(f.read())["GScript"])


class CustomEvents(Enum):
START_PRINT = "continuousprint_start_print"
COOLDOWN = "continuousprint_cooldown"
CLEAR_BED = "continuousprint_clear_bed"
FINISH = "continuousprint_finish"
CANCEL = "continuousprint_cancel"


class Keys(Enum):
# TODO migrate old setting names to enum names
QUEUE = ("cp_queue", None)
Expand Down
59 changes: 42 additions & 17 deletions continuousprint/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def __init__(
self._set_status("Initializing")
self.q = queue
self.state = self._state_unknown
self.idle_start_ts = None
self.last_printer_state = None
self.printer_state_ts = 0
self.printer_state_logs_suppressed = False
self.retries = 0
self.retry_on_pause = False
self.max_retries = 0
Expand All @@ -78,14 +80,21 @@ def action(
# Given that some calls to action() come from a watchdog timer, we hold a mutex when performing the action
# so the state is updated in a thread safe way.
with self.mutex:
self._logger.debug(
f"{a.name}, {p.name}, path={path}, materials={materials}, bed_temp={bed_temp}"
)
now = time.time()
if self.printer_state_ts + 15 > now or a != Action.TICK:
self._logger.debug(
f"{a.name}, {p.name}, path={path}, materials={materials}, bed_temp={bed_temp}"
)
elif a == Action.TICK and not self.printer_state_logs_suppressed:
self.printer_state_logs_suppressed = True
self._logger.debug(
f"suppressing further debug logs for action=TICK, printer state={p.name}"
)

if p == Printer.IDLE and self.idle_start_ts is None:
self.idle_start_ts = time.time()
elif p != Printer.IDLE and self.idle_start_ts is not None:
self.idle_start_ts = None
if p != self.last_printer_state:
self.printer_state_ts = now
self.last_printer_state = p
self.printer_state_logs_suppressed = False

if path is not None:
self._cur_path = path
Expand Down Expand Up @@ -127,7 +136,9 @@ def _state_inactive(self, a: Action, p: Printer):
if p == Printer.IDLE:
self._set_status("Inactive (click Start Managing)")
else:
self._set_status("Inactive (active print continues unmanaged)")
self._set_status(
"Inactive (active print continues unmanaged)", StatusType.NEEDS_ACTION
)

def _state_idle(self, a: Action, p: Printer):
self.q.release()
Expand All @@ -149,6 +160,15 @@ def _enter_start_print(self, a: Action, p: Printer):
nxt = self._state_start_print(a, p)
return nxt if nxt is not None else self._state_start_print

def _fmt_material_key(self, mk):
try:
s = mk.split("_")
return f"{s[0]} ({s[1]})"
except IndexError:
return mk
except AttributeError:
return mk

def _state_start_print(self, a: Action, p: Printer):
if p != Printer.IDLE:
self._set_status("Waiting for printer to be ready")
Expand All @@ -166,7 +186,7 @@ def _state_start_print(self, a: Action, p: Printer):
cur = self._cur_materials[i] if i < len(self._cur_materials) else None
if im != cur:
self._set_status(
f"Waiting for spool {im} in tool {i} (currently: {cur})",
f"Need {self._fmt_material_key(im)} in tool {i}, but {self._fmt_material_key(cur)} is loaded",
StatusType.NEEDS_ACTION,
)
return
Expand All @@ -186,6 +206,14 @@ def _state_start_print(self, a: Action, p: Printer):
StatusType.ERROR,
)

def _long_idle(self, p):
# We wait until we're in idle state for a long-ish period before acting, as
# IDLE can be returned as a state before another event-based action (e.g. SUCCESS)
return (
p == Printer.IDLE
and time.time() - self.printer_state_ts > self.PRINTING_IDLE_BREAKOUT_SEC
)

def _state_printing(self, a: Action, p: Printer, elapsed=None):
if a == Action.FAILURE:
return self._state_failure
Expand All @@ -200,10 +228,7 @@ def _state_printing(self, a: Action, p: Printer, elapsed=None):
StatusType.NEEDS_ACTION,
)
return self._state_paused
elif a == Action.SUCCESS or (
p == Printer.IDLE
and time.time() - self.idle_start_ts > self.PRINTING_IDLE_BREAKOUT_SEC
):
elif a == Action.SUCCESS or self._long_idle(p):
# If idle state without event, assume we somehow missed the SUCCESS action.
# We wait for a period of idleness to prevent idle-before-success events
# from double-completing prints.
Expand Down Expand Up @@ -231,7 +256,7 @@ def _state_printing(self, a: Action, p: Printer, elapsed=None):

def _state_paused(self, a: Action, p: Printer):
self._set_status("Paused", StatusType.NEEDS_ACTION)
if p == Printer.IDLE:
if self._long_idle(p):
# Here, IDLE implies the user cancelled the print.
# Go inactive to prevent stomping on manual changes
return self._state_inactive
Expand Down Expand Up @@ -314,7 +339,7 @@ def _state_clearing(self, a: Action, p: Printer):
self._set_status("Error when clearing bed - aborting", StatusType.ERROR)
return self._state_inactive # Skip past failure state to inactive

if p == Printer.IDLE: # Idle state without event; assume success
if self._long_idle(p): # Idle state without event; assume success
return self._enter_start_print(a, p)
else:
self._set_status("Clearing bed")
Expand All @@ -332,7 +357,7 @@ def _state_finishing(self, a: Action, p: Printer):
return self._state_inactive

# Idle state without event -> assume success and go idle
if a == Action.SUCCESS or p == Printer.IDLE:
if a == Action.SUCCESS or self._long_idle(p):
return self._state_idle

self._set_status("Finishing up")
Expand Down
3 changes: 2 additions & 1 deletion continuousprint/driver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_idle_while_printing(self):
self.assertEqual(self.d.state.__name__, self.d._state_printing.__name__)

# Continued idleness triggers bed clearing and such
self.d.idle_start_ts = time.time() - (Driver.PRINTING_IDLE_BREAKOUT_SEC + 1)
self.d.printer_state_ts = time.time() - (Driver.PRINTING_IDLE_BREAKOUT_SEC + 1)
self.d.action(DA.TICK, DP.IDLE)
self.assertEqual(self.d.state.__name__, self.d._state_start_clearing.__name__)

Expand Down Expand Up @@ -153,6 +153,7 @@ def test_completed_last_print(self):
) # -> success
self.d.q.get_set_or_acquire.return_value = None # Nothing more in the queue
self.d.action(DA.TICK, DP.IDLE) # -> start_finishing
self.d.printer_state_ts = time.time() - (Driver.PRINTING_IDLE_BREAKOUT_SEC + 1)
self.d.action(DA.TICK, DP.IDLE) # -> finishing
self.d._runner.run_finish_script.assert_called()
self.assertEqual(self.d.state.__name__, self.d._state_finishing.__name__)
Expand Down
Loading

0 comments on commit 53ba737

Please sign in to comment.