diff --git a/src/octoprint/plugins/virtual_printer/virtual.py b/src/octoprint/plugins/virtual_printer/virtual.py
index ecca69a9..632cddf4 100644
--- a/src/octoprint/plugins/virtual_printer/virtual.py
+++ b/src/octoprint/plugins/virtual_printer/virtual.py
@@ -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)
diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py
index 7351ade1..a5230811 100644
--- a/src/octoprint/server/api/settings.py
+++ b/src/octoprint/server/api/settings.py
@@ -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"])
diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py
index 0e525274..396bc0bc 100644
--- a/src/octoprint/settings.py
+++ b/src/octoprint/settings.py
@@ -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
+ }
}
}
}
diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js
index d15f42d0..e481f38d 100644
--- a/src/octoprint/static/js/app/viewmodels/settings.js
+++ b/src/octoprint/static/js/app/viewmodels/settings.js
@@ -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);
diff --git a/src/octoprint/templates/dialogs/settings/serialconnection.jinja2 b/src/octoprint/templates/dialogs/settings/serialconnection.jinja2
index 0beeae73..528889eb 100644
--- a/src/octoprint/templates/dialogs/settings/serialconnection.jinja2
+++ b/src/octoprint/templates/dialogs/settings/serialconnection.jinja2
@@ -19,7 +19,7 @@
-
+
@@ -35,6 +35,16 @@
{{ _('When a target temperature is set')}}
+
+
+
+
+
+ s
+
+ {{ _('Autoreport interval to request from firmware')}}
+
+
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py
index e409ca2c..c1c30d3b 100644
--- a/src/octoprint/util/comm.py
+++ b/src/octoprint/util/comm.py
@@ -62,6 +62,7 @@ regexes_parameters = dict(
floatY=re.compile("(^|[^A-Za-z])[Yy](?P%s)" % regex_float_pattern),
floatZ=re.compile("(^|[^A-Za-z])[Zz](?P%s)" % regex_float_pattern),
intN=re.compile("(^|[^A-Za-z])[Nn](?P%s)" % regex_int_pattern),
+ intS=re.compile("(^|[^A-Za-z])[Ss](?P%s)" % regex_int_pattern),
intT=re.compile("(^|[^A-Za-z])[Tt](?P%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::<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.
diff --git a/tests/util/test_comm_helpers.py b/tests/util/test_comm_helpers.py
index b3eb49b6..3042bfe5 100644
--- a/tests/util/test_comm_helpers.py
+++ b/tests/util/test_comm_helpers.py
@@ -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),