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:
Gina Häußge 2017-06-06 18:46:23 +02:00
parent a2fd39b3c4
commit 6a1b162e7b
7 changed files with 154 additions and 16 deletions

View file

@ -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)

View file

@ -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"])

View file

@ -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
}
}
}
}

View file

@ -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);

View file

@ -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">

View file

@ -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.

View file

@ -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),