Better tracking of printer connection state
Introduced three new events:
* CONNECTING - fired just before starting the connection process
* DISCONNECTING - fired just before starting the (active) disconnection
process
* PRINTER_STATE_CHANGED - fired every time the printer state changes
Also introduced new GCODE script beforePrinterDisconnected, which will
get sent just before the printer gets (actively) disconnected. Also enabled
communication object to wait for the send queue to be emptied before
closing it, in order to allow sending all lines from the disconnect script.
This commit is contained in:
parent
c516b91a6d
commit
7f2476e513
9 changed files with 194 additions and 73 deletions
|
|
@ -106,6 +106,9 @@ ClientClosed
|
|||
Printer communication
|
||||
---------------------
|
||||
|
||||
Connecting
|
||||
The server is attempting to connect to the printer.
|
||||
|
||||
Connected
|
||||
The server has connected to the printer.
|
||||
|
||||
|
|
@ -114,6 +117,11 @@ Connected
|
|||
* ``port``: the connected serial port
|
||||
* ``baudrate``: the baud rate
|
||||
|
||||
Disconnecting
|
||||
The server is going to disconnect from the printer. Note that this
|
||||
event might not always be sent when the server and printer get disconnected
|
||||
from each other. Do not depend on this for critical life cycle management.
|
||||
|
||||
Disconnected
|
||||
The server has disconnected from the printer
|
||||
|
||||
|
|
@ -124,6 +132,15 @@ Error
|
|||
|
||||
* ``error``: the error string
|
||||
|
||||
PrinterStateChanged
|
||||
The state of the printer changed.
|
||||
|
||||
Payload:
|
||||
|
||||
* ``state_id``: Id of the new state. See
|
||||
:func:`~octoprint.printer.PrinterInterface.get_state_id` for possible values.
|
||||
* ``state_string``: Text representation of the new state.
|
||||
|
||||
File handling
|
||||
-------------
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ Predefined Scripts
|
|||
The following GCODE scripts are sent by OctoPrint automatically:
|
||||
|
||||
* ``afterPrinterConnected``: Sent after OctoPrint successfully connected to a printer. Defaults to an empty script.
|
||||
* ``beforePrinterDisconnected``: Sent just before OctoPrint (actively) closes the connection to the printer. Defaults
|
||||
to an empty script. Note that this will *not* be sent for unexpected connection cut offs, e.g. in case of errors
|
||||
on the serial line, only when the user clicks the "Disconnect" button or the printer requests a disconnect via an
|
||||
:ref:`action command <sec-features-action_commands>` .
|
||||
* ``beforePrintStarted``: Sent just before a print job is started. Defaults to an empty script.
|
||||
* ``afterPrintCancelled``: Sent just after a print job was cancelled. Defaults to the
|
||||
:ref:`bundled script listed below <sec-features-gcode_scripts-bundled>`.
|
||||
|
|
|
|||
|
|
@ -28,9 +28,14 @@ class Events(object):
|
|||
STARTUP = "Startup"
|
||||
|
||||
# connect/disconnect to printer
|
||||
CONNECTING = "Connecting"
|
||||
CONNECTED = "Connected"
|
||||
DISCONNECTING = "Disconnecting"
|
||||
DISCONNECTED = "Disconnected"
|
||||
|
||||
# State changes
|
||||
PRINTER_STATE_CHANGED = "PrinterStateChanged"
|
||||
|
||||
# connect/disconnect by client
|
||||
CLIENT_OPENED = "ClientOpened"
|
||||
CLIENT_CLOSED = "ClientClosed"
|
||||
|
|
|
|||
|
|
@ -259,6 +259,31 @@ class PrinterInterface(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_state_id(self):
|
||||
"""
|
||||
Identifier of the current communication state.
|
||||
|
||||
Possible values are:
|
||||
|
||||
* OPEN_SERIAL
|
||||
* DETECT_SERIAL
|
||||
* DETECT_BAUDRATE
|
||||
* CONNECTING
|
||||
* OPERATIONAL
|
||||
* PRINTING
|
||||
* PAUSED
|
||||
* CLOSED
|
||||
* ERROR
|
||||
* CLOSED_WITH_ERROR
|
||||
* TRANFERING_FILE
|
||||
* OFFLINE
|
||||
* UNKNOWN
|
||||
* NONE
|
||||
|
||||
Returns:
|
||||
(str) A unique identifier corresponding to the current communication state.
|
||||
"""
|
||||
|
||||
def get_current_data(self):
|
||||
"""
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -191,7 +191,9 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
will be attempted.
|
||||
"""
|
||||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self.disconnect()
|
||||
|
||||
eventManager().fire(Events.CONNECTING)
|
||||
self._printerProfileManager.select(profile)
|
||||
self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager)
|
||||
|
||||
|
|
@ -199,11 +201,11 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
"""
|
||||
Closes the connection to the printer.
|
||||
"""
|
||||
eventManager().fire(Events.DISCONNECTING)
|
||||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
self._printerProfileManager.deselect()
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
else:
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
def get_transport(self):
|
||||
|
||||
|
|
@ -426,14 +428,17 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
payload["origin"] = FileDestinations.SDCARD
|
||||
eventManager().fire(Events.PRINT_FAILED, payload)
|
||||
|
||||
def get_state_string(self):
|
||||
"""
|
||||
Returns a human readable string corresponding to the current communication state.
|
||||
"""
|
||||
def get_state_string(self, state=None):
|
||||
if self._comm is None:
|
||||
return "Offline"
|
||||
else:
|
||||
return self._comm.getStateString()
|
||||
return self._comm.getStateString(state=state)
|
||||
|
||||
def get_state_id(self, state=None):
|
||||
if self._comm is None:
|
||||
return "OFFLINE"
|
||||
else:
|
||||
return self._comm.getStateId(state=state)
|
||||
|
||||
def get_current_data(self):
|
||||
return self._stateMonitor.get_current_data()
|
||||
|
|
@ -561,6 +566,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
self._state = state
|
||||
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
|
||||
|
||||
payload = dict(
|
||||
state_id=self.get_state_id(self._state),
|
||||
state_string=self.get_state_string(self._state)
|
||||
)
|
||||
eventManager().fire(Events.PRINTER_STATE_CHANGED, payload)
|
||||
|
||||
def _addLog(self, log):
|
||||
self._log.append(log)
|
||||
self._stateMonitor.add_log(log)
|
||||
|
|
@ -775,6 +786,8 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
self._setJobData(None, None, None)
|
||||
self._printerProfileManager.deselect()
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
self._setState(state)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ def getSettings():
|
|||
"scripts": {
|
||||
"gcode": {
|
||||
"afterPrinterConnected": None,
|
||||
"beforePrinterDisconnected": None,
|
||||
"beforePrintStarted": None,
|
||||
"afterPrintCancelled": None,
|
||||
"afterPrintDone": None,
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused = ko.observable(undefined);
|
||||
self.scripts_gcode_beforePrintResumed = ko.observable(undefined);
|
||||
self.scripts_gcode_afterPrinterConnected = ko.observable(undefined);
|
||||
self.scripts_gcode_beforePrinterDisconnected = ko.observable(undefined);
|
||||
|
||||
self.temperature_profiles = ko.observableArray(undefined);
|
||||
self.temperature_cutoff = ko.observable(undefined);
|
||||
|
|
@ -440,6 +441,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused);
|
||||
self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed);
|
||||
self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected);
|
||||
self.scripts_gcode_beforePrinterDisconnected(response.scripts.gcode.beforePrinterDisconnected);
|
||||
|
||||
self.temperature_profiles(response.temperature.profiles);
|
||||
self.temperature_cutoff(response.temperature.cutoff);
|
||||
|
|
@ -535,7 +537,8 @@ $(function() {
|
|||
"afterPrintCancelled": self.scripts_gcode_afterPrintCancelled(),
|
||||
"afterPrintPaused": self.scripts_gcode_afterPrintPaused(),
|
||||
"beforePrintResumed": self.scripts_gcode_beforePrintResumed(),
|
||||
"afterPrinterConnected": self.scripts_gcode_afterPrinterConnected()
|
||||
"afterPrinterConnected": self.scripts_gcode_afterPrinterConnected(),
|
||||
"beforePrinterDisconnected": self.scripts_gcode_beforePrinterDisconnected()
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
|
|
|
|||
|
|
@ -35,4 +35,11 @@
|
|||
<textarea rows="4" class="block" data-bind="value: scripts_gcode_afterPrinterConnected"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Before connection to printer is closed') }}</label>
|
||||
<div class="controls">
|
||||
<textarea rows="4" class="block" data-bind="value: scripts_gcode_beforePrinterDisconnected"></textarea>
|
||||
</div>
|
||||
<small>{{ _('This will only be executed when closing the connection actively. If the connection to the printer is suddenly lost nothing will be sent.') }}</small>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -271,37 +271,51 @@ class MachineCom(object):
|
|||
def getState(self):
|
||||
return self._state
|
||||
|
||||
def getStateString(self):
|
||||
if self._state == self.STATE_NONE:
|
||||
def getStateId(self, state=None):
|
||||
if state is None:
|
||||
state = self._state
|
||||
|
||||
possible_states = filter(lambda x: x.startswith("STATE_"), self.__class__.__dict__.keys())
|
||||
for possible_state in possible_states:
|
||||
if getattr(self, possible_state) == state:
|
||||
return possible_state[len("STATE_"):]
|
||||
|
||||
return "UNKNOWN"
|
||||
|
||||
def getStateString(self, state=None):
|
||||
if state is None:
|
||||
state = self._state
|
||||
|
||||
if state == self.STATE_NONE:
|
||||
return "Offline"
|
||||
if self._state == self.STATE_OPEN_SERIAL:
|
||||
if state == self.STATE_OPEN_SERIAL:
|
||||
return "Opening serial port"
|
||||
if self._state == self.STATE_DETECT_SERIAL:
|
||||
if state == self.STATE_DETECT_SERIAL:
|
||||
return "Detecting serial port"
|
||||
if self._state == self.STATE_DETECT_BAUDRATE:
|
||||
if state == self.STATE_DETECT_BAUDRATE:
|
||||
return "Detecting baudrate"
|
||||
if self._state == self.STATE_CONNECTING:
|
||||
if state == self.STATE_CONNECTING:
|
||||
return "Connecting"
|
||||
if self._state == self.STATE_OPERATIONAL:
|
||||
if state == self.STATE_OPERATIONAL:
|
||||
return "Operational"
|
||||
if self._state == self.STATE_PRINTING:
|
||||
if state == self.STATE_PRINTING:
|
||||
if self.isSdFileSelected():
|
||||
return "Printing from SD"
|
||||
elif self.isStreaming():
|
||||
return "Sending file to SD"
|
||||
else:
|
||||
return "Printing"
|
||||
if self._state == self.STATE_PAUSED:
|
||||
if state == self.STATE_PAUSED:
|
||||
return "Paused"
|
||||
if self._state == self.STATE_CLOSED:
|
||||
if state == self.STATE_CLOSED:
|
||||
return "Closed"
|
||||
if self._state == self.STATE_ERROR:
|
||||
if state == self.STATE_ERROR:
|
||||
return "Error: %s" % (self.getErrorString())
|
||||
if self._state == self.STATE_CLOSED_WITH_ERROR:
|
||||
if state == self.STATE_CLOSED_WITH_ERROR:
|
||||
return "Error: %s" % (self.getErrorString())
|
||||
if self._state == self.STATE_TRANSFERING_FILE:
|
||||
if state == self.STATE_TRANSFERING_FILE:
|
||||
return "Transfering file to SD"
|
||||
return "?%d?" % (self._state)
|
||||
return "Unknown State (%d)" % (self._state)
|
||||
|
||||
def getErrorString(self):
|
||||
return self._errorValue
|
||||
|
|
@ -382,7 +396,25 @@ class MachineCom(object):
|
|||
|
||||
##~~ external interface
|
||||
|
||||
def close(self, isError = False):
|
||||
def close(self, is_error=False, wait=True, *args, **kwargs):
|
||||
"""
|
||||
Closes the connection to the printer.
|
||||
|
||||
If ``is_error`` is False, will attempt to send the ``beforePrinterDisconnected``
|
||||
gcode script. If ``is_error`` is False and ``wait`` is True, will wait
|
||||
until all messages in the send queue (including the ``beforePrinterDisconnected``
|
||||
gcode script) have been sent to the printer.
|
||||
|
||||
Arguments:
|
||||
is_error (bool): Whether the closing takes place due to an error (True)
|
||||
or not (False, default)
|
||||
wait (bool): Whether to wait for all messages in the send
|
||||
queue to be processed before closing (True, default) or not (False)
|
||||
"""
|
||||
|
||||
# legacy parameters
|
||||
is_error = kwargs.get("isError", is_error)
|
||||
|
||||
if self._temperature_timer is not None:
|
||||
try:
|
||||
self._temperature_timer.cancel()
|
||||
|
|
@ -395,16 +427,25 @@ class MachineCom(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
self._monitoring_active = False
|
||||
self._send_queue_active = False
|
||||
def deactivate_monitoring_and_send_queue():
|
||||
self._monitoring_active = False
|
||||
self._send_queue_active = False
|
||||
|
||||
printing = self.isPrinting() or self.isPaused()
|
||||
if self._serial is not None:
|
||||
if isError:
|
||||
if not is_error:
|
||||
self.sendGcodeScript("beforePrinterDisconnected")
|
||||
if wait:
|
||||
self._send_queue.join()
|
||||
|
||||
deactivate_monitoring_and_send_queue()
|
||||
self._serial.close()
|
||||
if is_error:
|
||||
self._changeState(self.STATE_CLOSED_WITH_ERROR)
|
||||
else:
|
||||
self._changeState(self.STATE_CLOSED)
|
||||
self._serial.close()
|
||||
else:
|
||||
deactivate_monitoring_and_send_queue()
|
||||
self._serial = None
|
||||
|
||||
if settings().getBoolean(["feature", "sdSupport"]):
|
||||
|
|
@ -419,7 +460,6 @@ class MachineCom(object):
|
|||
"origin": self._currentFile.getFileLocation()
|
||||
}
|
||||
eventManager().fire(Events.PRINT_FAILED, payload)
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
def setTemperatureOffset(self, offsets):
|
||||
self._tempOffsets.update(offsets)
|
||||
|
|
@ -1055,7 +1095,7 @@ class MachineCom(object):
|
|||
except:
|
||||
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string()))
|
||||
else:
|
||||
self.close()
|
||||
self.close(wait=False)
|
||||
self._errorValue = "No more baudrates to test, and no suitable baudrate found."
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
|
|
@ -1072,7 +1112,7 @@ class MachineCom(object):
|
|||
elif "ok" in line:
|
||||
self._onConnected()
|
||||
elif time.time() > self._timeout:
|
||||
self.close()
|
||||
self.close(wait=False)
|
||||
|
||||
### Operational
|
||||
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
|
||||
|
|
@ -1324,7 +1364,7 @@ class MachineCom(object):
|
|||
except:
|
||||
self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
self.close(is_error=True)
|
||||
return None
|
||||
if ret == '':
|
||||
#self._log("Recv: TIMEOUT")
|
||||
|
|
@ -1509,52 +1549,58 @@ class MachineCom(object):
|
|||
# wait until we have something in the queue
|
||||
entry = self._send_queue.get()
|
||||
|
||||
# make sure we are still active
|
||||
if not self._send_queue_active:
|
||||
break
|
||||
try:
|
||||
# make sure we are still active
|
||||
if not self._send_queue_active:
|
||||
break
|
||||
|
||||
# fetch command and optional linenumber from queue
|
||||
command, linenumber, command_type = entry
|
||||
# fetch command and optional linenumber from queue
|
||||
command, linenumber, command_type = entry
|
||||
|
||||
# some firmwares (e.g. Smoothie) might support additional in-band communication that will not
|
||||
# stick to the acknowledgement behaviour of GCODE, so we check here if we have a GCODE command
|
||||
# at hand here and only clear our clear_to_send flag later if that's the case
|
||||
gcode = self.gcode_command_for_cmd(command)
|
||||
# some firmwares (e.g. Smoothie) might support additional in-band communication that will not
|
||||
# stick to the acknowledgement behaviour of GCODE, so we check here if we have a GCODE command
|
||||
# at hand here and only clear our clear_to_send flag later if that's the case
|
||||
gcode = self.gcode_command_for_cmd(command)
|
||||
|
||||
if linenumber is not None:
|
||||
# line number predetermined - this only happens for resends, so we'll use the number and
|
||||
# send directly without any processing (since that already took place on the first sending!)
|
||||
self._doSendWithChecksum(command, linenumber)
|
||||
|
||||
else:
|
||||
# trigger "sending" phase
|
||||
command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode)
|
||||
|
||||
if command is None:
|
||||
# so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue
|
||||
continue
|
||||
|
||||
# now comes the part where we increase line numbers and send stuff - no turning back now
|
||||
if (gcode is not None or self._sendChecksumWithUnknownCommands) and (self.isPrinting() or self._alwaysSendChecksum):
|
||||
linenumber = self._currentLine
|
||||
self._addToLastLines(command)
|
||||
self._currentLine += 1
|
||||
if linenumber is not None:
|
||||
# line number predetermined - this only happens for resends, so we'll use the number and
|
||||
# send directly without any processing (since that already took place on the first sending!)
|
||||
self._doSendWithChecksum(command, linenumber)
|
||||
|
||||
else:
|
||||
self._doSendWithoutChecksum(command)
|
||||
# trigger "sending" phase
|
||||
command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode)
|
||||
|
||||
# trigger "sent" phase and use up one "ok"
|
||||
self._process_command_phase("sent", command, command_type, gcode=gcode)
|
||||
if command is None:
|
||||
# so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue
|
||||
continue
|
||||
|
||||
# we only need to use up a clear if the command we just sent was either a gcode command or if we also
|
||||
# require ack's for unknown commands
|
||||
use_up_clear = self._unknownCommandsNeedAck
|
||||
if gcode is not None:
|
||||
use_up_clear = True
|
||||
# now comes the part where we increase line numbers and send stuff - no turning back now
|
||||
if (gcode is not None or self._sendChecksumWithUnknownCommands) and (self.isPrinting() or self._alwaysSendChecksum):
|
||||
linenumber = self._currentLine
|
||||
self._addToLastLines(command)
|
||||
self._currentLine += 1
|
||||
self._doSendWithChecksum(command, linenumber)
|
||||
else:
|
||||
self._doSendWithoutChecksum(command)
|
||||
|
||||
# if we need to use up a clear, do that now
|
||||
if use_up_clear:
|
||||
self._clear_to_send.clear()
|
||||
# trigger "sent" phase and use up one "ok"
|
||||
self._process_command_phase("sent", command, command_type, gcode=gcode)
|
||||
|
||||
# we only need to use up a clear if the command we just sent was either a gcode command or if we also
|
||||
# require ack's for unknown commands
|
||||
use_up_clear = self._unknownCommandsNeedAck
|
||||
if gcode is not None:
|
||||
use_up_clear = True
|
||||
|
||||
# if we need to use up a clear, do that now
|
||||
if use_up_clear:
|
||||
self._clear_to_send.clear()
|
||||
|
||||
finally:
|
||||
# no matter _how_ we exit this block, we signal that we
|
||||
# are done processing the last fetched queue entry
|
||||
self._send_queue.task_done()
|
||||
|
||||
# now we just wait for the next clear and then start again
|
||||
self._clear_to_send.wait()
|
||||
|
|
@ -1645,12 +1691,12 @@ class MachineCom(object):
|
|||
self._logger.exception("Unexpected error while writing to serial port")
|
||||
self._log("Unexpected error while writing to serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
self.close(is_error=True)
|
||||
except:
|
||||
self._logger.exception("Unexpected error while writing to serial port")
|
||||
self._log("Unexpected error while writing to serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
self.close(is_error=True)
|
||||
|
||||
##~~ command handlers
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue