Add (optional) firmware auto detection

If enabled (which it is by default), OctoPrint will now send
an M115 to the printer on initial connection in order to try
to figure out what kind of firmware it is. For FIRMWARE_NAME
values containing "repetier" (case insensitive), all Repetier-
specific flags will be set on the comm layer. For FIRMWARE_NAME
values containing "reprapfirmware", all RepRapFirmware-
specific flags will be set on the comm layer.

For now no other handling will be performed.
This commit is contained in:
Gina Häußge 2016-11-17 17:13:36 +01:00
parent ec12e52865
commit 07382d3918
6 changed files with 124 additions and 44 deletions

View file

@ -393,7 +393,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._printAfterSelect = printAfterSelect
self._posAfterSelect = pos
self._comm.selectFile("/" + path if sd and not settings().getBoolean(["feature", "sdRelativePath"]) else path, sd)
self._comm.selectFile("/" + path if sd else path, sd)
self._setProgressData(completion=0)
self._setCurrentZ(None)

View file

@ -104,7 +104,8 @@ def getSettings():
"keyboardControl": s.getBoolean(["feature", "keyboardControl"]),
"pollWatched": s.getBoolean(["feature", "pollWatched"]),
"ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"]),
"modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"])
"modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"]),
"firmwareDetection": s.getBoolean(["feature", "firmwareDetection"])
},
"serial": {
"port": connectionOptions["portPreference"],
@ -274,6 +275,7 @@ def _saveSettings(data):
if "pollWatched" in data["feature"]: s.setBoolean(["feature", "pollWatched"], data["feature"]["pollWatched"])
if "ignoreIdenticalResends" in data["feature"]: s.setBoolean(["feature", "ignoreIdenticalResends"], data["feature"]["ignoreIdenticalResends"])
if "modelSizeDetection" in data["feature"]: s.setBoolean(["feature", "modelSizeDetection"], data["feature"]["modelSizeDetection"])
if "firmwareDetection" in data["feature"]: s.setBoolean(["feature", "firmwareDetection"], data["feature"]["firmwareDetection"])
if "serial" in data.keys():
if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"])

View file

@ -197,7 +197,8 @@ default_settings = {
"ignoreIdenticalResends": False,
"identicalResendsCountdown": 7,
"supportFAsCommand": False,
"modelSizeDetection": True
"modelSizeDetection": True,
"firmwareDetection": True
},
"folder": {
"uploads": None,

View file

@ -133,6 +133,7 @@ $(function() {
self.feature_pollWatched = ko.observable(undefined);
self.feature_ignoreIdenticalResends = ko.observable(undefined);
self.feature_modelSizeDetection = ko.observable(undefined);
self.feature_firmwareDetection = ko.observable(undefined);
self.serial_port = ko.observable();
self.serial_baudrate = ko.observable();

View file

@ -34,53 +34,67 @@
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdRelativePath" id="settings-sdRelativePath"> {{ _('Select SD files by relative path') }} <span class="label">{{ _('RepRap Firmware') }}</span>
<input type="checkbox" data-bind="checked: feature_firmwareDetection" id="settings-firmwareDetection"> {{ _('Enable automatic firmware detection') }}
<span class="help-inline">{% trans %}
If enabled, OctoPrint will try to figure out your printer's firmware automatically and adjust a couple of communication parameters based on that.
If that doesn't work out, or you want more granular control, uncheck this and the parameters in question will become visible for you to adjust.
{% endtrans %}</span>
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdAlwaysAvailable" id="settings-featureSdAlwaysAvailable"> {{ _('Always assume SD card is present') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<div data-bind="visible: !feature_firmwareDetection()">
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdRelativePath" id="settings-sdRelativePath"> {{ _('Select SD files by relative path') }} <span class="label">{{ _('RepRap Firmware') }}</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_ignoreIdenticalResends" id="settings-ignoreIdenticalResends"> {{ _('Ignore consecutive resend requests for the same line') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdAlwaysAvailable" id="settings-featureSdAlwaysAvailable"> {{ _('Always assume SD card is present') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_repetierTargetTemp" id="settings-featureRepetierTargetTemp"> {{ _('Support <code>TargetExtr%%n</code>/<code>TargetBed</code> target temperature format') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_ignoreIdenticalResends" id="settings-ignoreIdenticalResends"> {{ _('Ignore consecutive resend requests for the same line') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_disableExternalHeatupDetection" id="settings-featureExternalHeatupDetection"> {{ _('Disable detection of external heatups') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_repetierTargetTemp" id="settings-featureRepetierTargetTemp"> {{ _('Support <code>TargetExtr%%n</code>/<code>TargetBed</code> target temperature format') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Send a checksum with the command')}}</label>
<div class="controls">
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="print" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumPrint"> {{ _('When printing') }}
</label>
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="always" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumAlways"> {{ _('Always') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="never" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumNever"> {{ _('Never') }}
</label>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_disableExternalHeatupDetection" id="settings-featureExternalHeatupDetection"> {{ _('Disable detection of external heatups') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Send a checksum with the command')}}</label>
<div class="controls">
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="print" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumPrint"> {{ _('When printing') }}
</label>
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="always" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumAlways"> {{ _('Always') }} <span class="label">{{ _('Repetier') }}</span>
</label>
<label class="radio">
<input type="radio" name="sendChecksumGroup" value="never" data-bind="checked: feature_sendChecksum" id="settings-featureSendChecksumNever"> {{ _('Never') }}
</label>
</div>
</div>
</div>
</form>

View file

@ -28,7 +28,7 @@ from octoprint.events import eventManager, Events
from octoprint.filemanager import valid_file_type
from octoprint.filemanager.destinations import FileDestinations
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer, \
to_unicode, bom_aware_open, TypedQueue, TypeAlreadyInQueue
to_unicode, bom_aware_open, TypedQueue, TypeAlreadyInQueue, chunks
try:
import _winreg
@ -124,6 +124,9 @@ Groups will be as follows:
* ``e``: E coordinate
"""
regex_firmware_splitter = re.compile("\s*([A-Z0-9_]+):")
"""Regex to use for splitting M115 responses."""
def serialList():
baselist=[]
if os.name=="nt":
@ -310,6 +313,8 @@ class MachineCom(object):
self._neverSendChecksum = settings().getBoolean(["feature", "neverSendChecksum"])
self._sendChecksumWithUnknownCommands = settings().getBoolean(["feature", "sendChecksumWithUnknownCommands"])
self._unknownCommandsNeedAck = settings().getBoolean(["feature", "unknownCommandsNeedAck"])
self._sdAlwaysAvailable = settings().getBoolean(["feature", "sdAlwaysAvailable"])
self._sdRelativePath = settings().getBoolean(["feature", "sdRelativePath"])
self._currentLine = 1
self._line_mutex = threading.RLock()
self._resendDelta = None
@ -319,7 +324,9 @@ class MachineCom(object):
self._currentResendCount = 0
self._resendSwallowRepetitions = settings().getBoolean(["feature", "ignoreIdenticalResends"])
self._resendSwallowRepetitionsCounter = 0
self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"])
self._firmwareDetection = settings().getBoolean(["feature", "firmwareDetection"])
self._firmwareInfoReceived = not self._firmwareDetection
self._supportResendsWithoutOk = settings().getBoolean(["serial", "supportResendsWithoutOk"])
@ -795,6 +802,10 @@ class MachineCom(object):
if not self.isOperational():
# printer is not connected, can't use SD
return
if filename.startswith("/") and self._sdRelativePath:
filename = filename[1:]
self._sdFileToSelect = filename
self.sendCommand("M23 %s" % filename)
else:
@ -920,7 +931,7 @@ class MachineCom(object):
return
self.sendCommand("M21")
if settings().getBoolean(["feature", "sdAlwaysAvailable"]):
if self._sdAlwaysAvailable:
self._sdAvailable = True
self.refreshSdFiles()
self._callback.on_comm_sd_state_change(self._sdAvailable)
@ -1207,6 +1218,33 @@ class MachineCom(object):
except ValueError:
pass
##~~ firmware name & version
elif "FIRMWARE_NAME:" in line:
# looks like a response to M115
data = parse_firmware_line(line)
firmware_name = data.get("FIRMWARE_NAME")
self._logger.info("Printer reports firmware name \"{}\"".format(firmware_name))
if not self._firmwareInfoReceived and firmware_name:
if "repetier" in firmware_name.lower():
self._logger.info("Detected Repetier firmware, enabling relevant features for issue free communication")
self._alwaysSendChecksum = True
self._resendSwallowRepetitions = True
supportRepetierTargetTemp = True
disable_external_heatup_detection = True
sd_always_available = self._sdAlwaysAvailable
self._sdAlwaysAvailable = True
if not sd_always_available and not self._sdAvailable:
self.initSdCard()
elif "reprapfirmware" in firmware_name.lower():
self._logger.info("Detected RepRapFirmware, enabling relevant features for issue free communication")
self._sdRelativePath = True
self._firmwareInfoReceived = True
##~~ SD Card handling
elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line:
self._sdAvailable = False
@ -1528,6 +1566,8 @@ class MachineCom(object):
self._changeState(self.STATE_OPERATIONAL)
self.resetLineNumbers()
if self._firmwareDetection:
self.sendCommand("M115")
if self._sdAvailable:
self.refreshSdFiles()
@ -2012,7 +2052,9 @@ class MachineCom(object):
# now comes the part where we increase line numbers and send stuff - no turning back now
command_requiring_checksum = gcode is not None and gcode in self._checksum_requiring_commands
command_allowing_checksum = gcode is not None or self._sendChecksumWithUnknownCommands
checksum_enabled = self.isPrinting() or self._alwaysSendChecksum
checksum_enabled = not self._neverSendChecksum and (self.isPrinting() or
self._alwaysSendChecksum or
not self._firmwareInfoReceived)
command_to_send = command.encode("ascii", errors="replace")
if command_requiring_checksum or (command_allowing_checksum and checksum_enabled):
@ -2916,6 +2958,26 @@ def parse_temperature_line(line, current):
return max(maxToolNum, current), canonicalize_temperatures(result, current)
def parse_firmware_line(line):
"""
Parses the provided firmware info line.
The result will be a dictionary mapping from the contained keys to the contained
values.
Arguments:
line (str): the line to parse
Returns:
dict: a dictionary with the parsed data
"""
result = dict()
split_line = regex_firmware_splitter.split(line.strip())[1:] # first entry is empty start of trimmed string
for key, value in chunks(split_line, 2):
result[key] = value
return result
def gcode_command_for_cmd(cmd):
"""
Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").