Support for temperature autoreporting instead of polling
Now also detects capabilities reported by (extended) M115 output and if AUTOREPORT_TEMP is available enables it with the configured autoreport interval and disables active polling. Implements #1679
This commit is contained in:
parent
a2fd39b3c4
commit
6a1b162e7b
7 changed files with 154 additions and 16 deletions
|
|
@ -18,6 +18,7 @@ from serial import SerialTimeoutException
|
|||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.plugin import plugin_manager
|
||||
from octoprint.util import RepeatedTimer
|
||||
|
||||
class VirtualPrinter(object):
|
||||
command_regex = re.compile("^([GMTF])(\d+)")
|
||||
|
|
@ -113,6 +114,10 @@ class VirtualPrinter(object):
|
|||
|
||||
self._okFormatString = settings().get(["devel", "virtualPrinter", "okFormatString"])
|
||||
|
||||
self._capabilities = settings().get(["devel", "virtualPrinter", "capabilities"])
|
||||
|
||||
self._temperature_reporter = None
|
||||
|
||||
self.currentLine = 0
|
||||
self.lastN = 0
|
||||
|
||||
|
|
@ -405,11 +410,26 @@ class VirtualPrinter(object):
|
|||
output = self._m115FormatString.format(firmware_name=self._firmwareName)
|
||||
self._send(output)
|
||||
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "m115ReportCapabilities"]):
|
||||
for cap, enabled in self._capabilities.items():
|
||||
self._send("Cap:{}:{}".format(cap.upper(), "1" if enabled else "0"))
|
||||
|
||||
def _gcode_M117(self, data):
|
||||
# we'll just use this to echo a message, to allow playing around with pause triggers
|
||||
if self._echoOnM117:
|
||||
self._send("echo:%s" % re.search("M117\s+(.*)", data).group(1))
|
||||
|
||||
def _gcode_M155(self, data):
|
||||
interval = int(re.search("S([0-9]+)", data).group(1))
|
||||
if self._temperature_reporter is not None:
|
||||
self._temperature_reporter.cancel()
|
||||
|
||||
if interval > 0:
|
||||
self._temperature_reporter = RepeatedTimer(interval, lambda: self._send(self._generateTemperatureOutput()))
|
||||
self._temperature_reporter.start()
|
||||
else:
|
||||
self._temperature_reporter = None
|
||||
|
||||
def _gcode_M220(self, data):
|
||||
self._feedrate_multiplier = float(re.search('S([0-9]+)', data).group(1))
|
||||
|
||||
|
|
@ -675,9 +695,8 @@ class VirtualPrinter(object):
|
|||
else:
|
||||
self._send("Not SD printing")
|
||||
|
||||
def _processTemperatureQuery(self):
|
||||
def _generateTemperatureOutput(self):
|
||||
includeTarget = not settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"])
|
||||
includeOk = not self._okBeforeCommandOutput
|
||||
|
||||
# send simulated temperature data
|
||||
if self.temperatureCount > 1:
|
||||
|
|
@ -709,6 +728,11 @@ class VirtualPrinter(object):
|
|||
output = "T:%.2f B:%.2f" % (self.temp[0], self.bedTemp)
|
||||
|
||||
output += " @:64\n"
|
||||
return output
|
||||
|
||||
def _processTemperatureQuery(self):
|
||||
includeOk = not self._okBeforeCommandOutput
|
||||
output = self._generateTemperatureOutput()
|
||||
|
||||
if includeOk:
|
||||
output = "{} {}".format(self._ok(), output)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ def getSettings():
|
|||
"timeoutCommunication": s.getFloat(["serial", "timeout", "communication"]),
|
||||
"timeoutTemperature": s.getFloat(["serial", "timeout", "temperature"]),
|
||||
"timeoutTemperatureTargetSet": s.getFloat(["serial", "timeout", "temperatureTargetSet"]),
|
||||
"timeoutTemperatureAutoreport": s.getFloat(["serial", "timeout", "temperatureAutoreport"]),
|
||||
"timeoutSdStatus": s.getFloat(["serial", "timeout", "sdStatus"]),
|
||||
"log": s.getBoolean(["serial", "log"]),
|
||||
"additionalPorts": s.get(["serial", "additionalPorts"]),
|
||||
|
|
@ -349,6 +350,7 @@ def _saveSettings(data):
|
|||
if "timeoutCommunication" in data["serial"]: s.setFloat(["serial", "timeout", "communication"], data["serial"]["timeoutCommunication"])
|
||||
if "timeoutTemperature" in data["serial"]: s.setFloat(["serial", "timeout", "temperature"], data["serial"]["timeoutTemperature"])
|
||||
if "timeoutTemperatureTargetSet" in data["serial"]: s.setFloat(["serial", "timeout", "temperatureTargetSet"], data["serial"]["timeoutTemperatureTargetSet"])
|
||||
if "timeoutTemperatureAutoreport" in data["serial"]: s.setFloat(["serial", "timeout", "temperatureAutoreport"], data["serial"]["timeoutTemperatureAutoreport"])
|
||||
if "timeoutSdStatus" in data["serial"]: s.setFloat(["serial", "timeout", "sdStatus"], data["serial"]["timeoutSdStatus"])
|
||||
if "additionalPorts" in data["serial"] and isinstance(data["serial"]["additionalPorts"], (list, tuple)): s.set(["serial", "additionalPorts"], data["serial"]["additionalPorts"])
|
||||
if "additionalBaudrates" in data["serial"] and isinstance(data["serial"]["additionalBaudrates"], (list, tuple)): s.set(["serial", "additionalBaudrates"], data["serial"]["additionalBaudrates"])
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ default_settings = {
|
|||
"communication": 30,
|
||||
"temperature": 5,
|
||||
"temperatureTargetSet": 2,
|
||||
"temperatureAutoreport": 2,
|
||||
"sdStatus": 1
|
||||
},
|
||||
"maxCommunicationTimeouts": {
|
||||
|
|
@ -299,7 +300,7 @@ default_settings = {
|
|||
"apps": {}
|
||||
},
|
||||
"terminalFilters": [
|
||||
{ "name": "Suppress temperature messages", "regex": "(Send: (N\d+\s+)?M105)|(Recv: ok (B|T\d*):)" },
|
||||
{ "name": "Suppress temperature messages", "regex": "(Send: (N\d+\s+)?M105)|(Recv:\s+(ok\s+)?(B|T\d*):)" },
|
||||
{ "name": "Suppress SD status messages", "regex": "(Send: (N\d+\s+)?M27)|(Recv: SD printing byte)" },
|
||||
{ "name": "Suppress wait responses", "regex": "Recv: wait"}
|
||||
],
|
||||
|
|
@ -364,7 +365,11 @@ default_settings = {
|
|||
"simulateReset": True,
|
||||
"preparedOks": [],
|
||||
"okFormatString": "ok",
|
||||
"m115FormatString": "FIRMWARE_NAME: {firmware_name} PROTOCOL_VERSION:1.0"
|
||||
"m115FormatString": "FIRMWARE_NAME: {firmware_name} PROTOCOL_VERSION:1.0",
|
||||
"m115ReportCapabilities": False,
|
||||
"capabilities": {
|
||||
"AUTOREPORT_TEMP": True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ $(function() {
|
|||
self.serial_timeoutCommunication = ko.observable(undefined);
|
||||
self.serial_timeoutTemperature = ko.observable(undefined);
|
||||
self.serial_timeoutTemperatureTargetSet = ko.observable(undefined);
|
||||
self.serial_timeoutTemperatureAutoreport = ko.observable(undefined);
|
||||
self.serial_timeoutSdStatus = ko.observable(undefined);
|
||||
self.serial_log = ko.observable(undefined);
|
||||
self.serial_additionalPorts = ko.observable(undefined);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ _('Interval in which to poll for the temperature information from the printer') }}">
|
||||
<label class="control-label" for="settings-serialTimeoutTemperature">{{ _('Temperature interval') }}</label>
|
||||
<label class="control-label" for="settings-serialTimeoutTemperature">{{ _('Temperature interval (polling)') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="1" class="input-mini text-right" data-bind="value: serial_timeoutTemperature" id="settings-serialTimeoutTemperature">
|
||||
|
|
@ -35,6 +35,16 @@
|
|||
<span class="help-inline">{{ _('When a target temperature is set')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ _('Temperature report interval to request from autoreport capable firmwares. A value of 0 disables autoreporting by the firmware and forces polling.') }}">
|
||||
<label class="control-label">{{ _('Temperature interval (autoreport)') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutTemperatureAutoreport" id="settings-serialTimeoutTemperatureAutoreport">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
<span class="help-inline">{{ _('Autoreport interval to request from firmware')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ _('Interval in which to poll for the SD printing status information from the printer while printing') }}">
|
||||
<label class="control-label" for="settings-serialTimeoutSdStatus">{{ _('SD status interval') }}</label>
|
||||
<div class="controls">
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ regexes_parameters = dict(
|
|||
floatY=re.compile("(^|[^A-Za-z])[Yy](?P<value>%s)" % regex_float_pattern),
|
||||
floatZ=re.compile("(^|[^A-Za-z])[Zz](?P<value>%s)" % regex_float_pattern),
|
||||
intN=re.compile("(^|[^A-Za-z])[Nn](?P<value>%s)" % regex_int_pattern),
|
||||
intS=re.compile("(^|[^A-Za-z])[Ss](?P<value>%s)" % regex_int_pattern),
|
||||
intT=re.compile("(^|[^A-Za-z])[Tt](?P<value>%s)" % regex_int_pattern)
|
||||
)
|
||||
"""Regexes for parsing various GCODE command parameters."""
|
||||
|
|
@ -254,6 +255,8 @@ class MachineCom(object):
|
|||
STATE_CLOSED_WITH_ERROR = 10
|
||||
STATE_TRANSFERING_FILE = 11
|
||||
|
||||
CAPABILITY_AUTOREPORT_TEMP = "AUTOREPORT_TEMP"
|
||||
|
||||
def __init__(self, port = None, baudrate=None, callbackObject=None, printerProfileManager=None):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._serialLogger = logging.getLogger("SERIAL")
|
||||
|
|
@ -336,8 +339,12 @@ class MachineCom(object):
|
|||
self._resendSwallowRepetitions = settings().getBoolean(["feature", "ignoreIdenticalResends"])
|
||||
self._resendSwallowRepetitionsCounter = 0
|
||||
|
||||
self._firmwareDetection = settings().getBoolean(["feature", "firmwareDetection"])
|
||||
self._firmwareInfoReceived = not self._firmwareDetection
|
||||
self._firmware_detection = settings().getBoolean(["feature", "firmwareDetection"])
|
||||
self._firmware_info_received = not self._firmware_detection
|
||||
self._firmware_info = dict()
|
||||
self._firmware_capabilities = dict()
|
||||
|
||||
self._temperature_autoreporting = False
|
||||
|
||||
self._supportResendsWithoutOk = settings().getBoolean(["serial", "supportResendsWithoutOk"])
|
||||
|
||||
|
|
@ -1212,11 +1219,16 @@ class MachineCom(object):
|
|||
self._callback.on_comm_position_update(self.last_position.as_dict(), reason=reason)
|
||||
|
||||
# temperature processing
|
||||
elif ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ((' B:' in line or line.startswith('B:')) and not 'A:' in line):
|
||||
if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating and self._firmwareInfoReceived:
|
||||
elif ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') \
|
||||
or ((' B:' in line or line.startswith('B:')) and not 'A:' in line):
|
||||
|
||||
if not disable_external_heatup_detection and not self._temperature_autoreporting \
|
||||
and not line.strip().startswith("ok") and not self._heating \
|
||||
and self._firmware_info_received:
|
||||
self._logger.debug("Externally triggered heatup detected")
|
||||
self._heating = True
|
||||
self._heatupWaitStartTime = time.time()
|
||||
|
||||
self._processTemperatures(line)
|
||||
self._callback.on_comm_temperature_update(self._temp, self._bedTemp)
|
||||
|
||||
|
|
@ -1268,7 +1280,7 @@ class MachineCom(object):
|
|||
if "malyan" in name.lower() and ver:
|
||||
firmware_name = name.strip() + " " + ver.strip()
|
||||
|
||||
if not self._firmwareInfoReceived and firmware_name:
|
||||
if not self._firmware_info_received and firmware_name:
|
||||
firmware_name = firmware_name.strip()
|
||||
self._logger.info("Printer reports firmware name \"{}\"".format(firmware_name))
|
||||
|
||||
|
|
@ -1301,7 +1313,20 @@ class MachineCom(object):
|
|||
if not sd_always_available and not self._sdAvailable:
|
||||
self.initSdCard()
|
||||
|
||||
self._firmwareInfoReceived = True
|
||||
self._firmware_info_received = True
|
||||
self._firmware_info = data
|
||||
self._firmware_name = firmware_name
|
||||
|
||||
##~~ Firmware capability report triggered by M115
|
||||
elif lower_line.startswith("cap:"):
|
||||
parsed = parse_capability_line(lower_line)
|
||||
if parsed is not None:
|
||||
capability, enabled = parsed
|
||||
self._firmware_capabilities[capability] = enabled
|
||||
|
||||
if capability == self.CAPABILITY_AUTOREPORT_TEMP and enabled:
|
||||
self._logger.info("Firmware states that it supports temperature autoreporting")
|
||||
self._set_autoreport_temperature()
|
||||
|
||||
##~~ SD Card handling
|
||||
elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line:
|
||||
|
|
@ -1613,11 +1638,11 @@ class MachineCom(object):
|
|||
"""
|
||||
Polls the temperature after the temperature timeout, re-enqueues itself.
|
||||
|
||||
If the printer is not operational, closing the connection, not printing from sd, busy with a long running
|
||||
command or heating, no poll will be done.
|
||||
If the printer is not operational, capable of auto-reporting temperatures, closing the connection, not printing
|
||||
from sd, busy with a long running command or heating, no poll will be done.
|
||||
"""
|
||||
|
||||
if self.isOperational() and not self._connection_closing and not self.isStreaming() and not self._long_running_command and not self._heating and not self._dwelling_until and not self._manualStreaming:
|
||||
if self.isOperational() and not self._temperature_autoreporting and not self._connection_closing and not self.isStreaming() and not self._long_running_command and not self._heating and not self._dwelling_until and not self._manualStreaming:
|
||||
self.sendCommand("M105", cmd_type="temperature_poll")
|
||||
|
||||
def _poll_sd_status(self):
|
||||
|
|
@ -1631,6 +1656,14 @@ class MachineCom(object):
|
|||
if self.isOperational() and not self._connection_closing and self.isSdPrinting() and not self._long_running_command and not self._dwelling_until and not self._heating:
|
||||
self.sendCommand("M27", cmd_type="sd_status_poll")
|
||||
|
||||
def _set_autoreport_temperature(self, interval=None):
|
||||
if interval is None:
|
||||
try:
|
||||
interval = int(self._timeout_intervals.get("temperatureAutoreport", 2))
|
||||
except:
|
||||
interval = 2
|
||||
self.sendCommand("M155 S{}".format(interval))
|
||||
|
||||
def _onConnected(self):
|
||||
self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"])
|
||||
self._temperature_timer = RepeatedTimer(self._getTemperatureTimerInterval, self._poll_temperature, run_first=True)
|
||||
|
|
@ -1639,7 +1672,7 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_OPERATIONAL)
|
||||
|
||||
self.resetLineNumbers()
|
||||
if self._firmwareDetection:
|
||||
if self._firmware_detection:
|
||||
self.sendCommand("M115")
|
||||
|
||||
if self._sdAvailable:
|
||||
|
|
@ -2176,7 +2209,7 @@ class MachineCom(object):
|
|||
command_allowing_checksum = gcode is not None or self._sendChecksumWithUnknownCommands
|
||||
checksum_enabled = not self._neverSendChecksum and (self.isPrinting() or
|
||||
self._alwaysSendChecksum or
|
||||
not self._firmwareInfoReceived)
|
||||
not self._firmware_info_received)
|
||||
|
||||
command_to_send = command.encode("ascii", errors="replace")
|
||||
if command_requiring_checksum or (command_allowing_checksum and checksum_enabled):
|
||||
|
|
@ -2484,6 +2517,16 @@ class MachineCom(object):
|
|||
self._long_running_command = True
|
||||
self._heating = True
|
||||
|
||||
def _gcode_M155_sending(self, cmd, cmd_type=None):
|
||||
match = regexes_parameters["intS"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
interval = int(match.group("value"))
|
||||
self._temperature_autoreporting = self._firmware_capabilities.get(self.CAPABILITY_AUTOREPORT_TEMP, False) \
|
||||
and (interval > 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _gcode_M110_sending(self, cmd, cmd_type=None):
|
||||
newLineNumber = 0
|
||||
match = regexes_parameters["intN"].search(cmd)
|
||||
|
|
@ -3111,6 +3154,43 @@ def parse_firmware_line(line):
|
|||
result[key] = value
|
||||
return result
|
||||
|
||||
def parse_capability_line(line):
|
||||
"""
|
||||
Parses the provided firmware capability line.
|
||||
|
||||
Lines are expected to be of the format
|
||||
|
||||
Cap:<capability name in caps>:<0 or 1>
|
||||
|
||||
e.g.
|
||||
|
||||
Cap:AUTOREPORT_TEMP:1
|
||||
Cap:TOGGLE_LIGHTS:0
|
||||
|
||||
Args:
|
||||
line (str): the line to parse
|
||||
|
||||
Returns:
|
||||
tuple: a 2-tuple of the parsed capability name and whether it's on (true) or off (false), or None if the line
|
||||
could not be parsed
|
||||
"""
|
||||
|
||||
line = line.lower()
|
||||
if line.startswith("cap:"):
|
||||
line = line[len("cap:"):]
|
||||
|
||||
parts = line.split(":")
|
||||
if len(parts) != 2:
|
||||
# wrong format, can't parse this
|
||||
return None
|
||||
|
||||
capability, flag = parts
|
||||
if not flag in ("0", "1"):
|
||||
# wrong format, can't parse this
|
||||
return None
|
||||
|
||||
return capability.upper(), flag == "1"
|
||||
|
||||
def parse_resend_line(line):
|
||||
"""
|
||||
Parses the provided resend line and returns requested line number.
|
||||
|
|
|
|||
|
|
@ -247,6 +247,22 @@ class TestCommHelpers(unittest.TestCase):
|
|||
result = parse_firmware_line(line)
|
||||
self.assertDictEqual(expected, result)
|
||||
|
||||
@data(
|
||||
("Cap:EEPROM:1", ("EEPROM", True)),
|
||||
("Cap:EEPROM:0", ("EEPROM", False)),
|
||||
("AUTOREPORT_TEMP:1", ("AUTOREPORT_TEMP", True)),
|
||||
("AUTOREPORT_TEMP:0", ("AUTOREPORT_TEMP", False)),
|
||||
("TOO:MANY:FIELDS", None),
|
||||
("Cap:", None),
|
||||
("TOOLITTLEFIELDS", None),
|
||||
("WRONG:FLAG", None),
|
||||
)
|
||||
@unpack
|
||||
def test_parse_capability_line(self, line, expected):
|
||||
from octoprint.util.comm import parse_capability_line
|
||||
result = parse_capability_line(line)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@data(
|
||||
("Resend:23", 23),
|
||||
("Resend: N23", 23),
|
||||
|
|
|
|||
Loading…
Reference in a new issue