diff --git a/octoprint_bambu_printer/bambu_print_plugin.py b/octoprint_bambu_printer/bambu_print_plugin.py index 13b48e9..8a989e4 100644 --- a/octoprint_bambu_printer/bambu_print_plugin.py +++ b/octoprint_bambu_printer/bambu_print_plugin.py @@ -65,7 +65,7 @@ class BambuPrintPlugin( "bed_leveling": True, "flow_cali": False, "vibration_cali": True, - "layer_inspect": True, + "layer_inspect": False, "use_ams": False, "local_mqtt": True, "region": "", diff --git a/octoprint_bambu_printer/bambu_virtual_printer.py b/octoprint_bambu_printer/bambu_virtual_printer.py index ea796f3..67426f7 100644 --- a/octoprint_bambu_printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/bambu_virtual_printer.py @@ -14,17 +14,21 @@ from typing import Any, Dict, List, Optional import asyncio from pybambu import BambuClient, commands import logging +import logging.handlers from serial import SerialTimeoutException from octoprint.util import RepeatedTimer, to_bytes, to_unicode, get_dos_filename from octoprint.util.files import unix_timestamp_to_m20_timestamp +from octoprint_bambu_printer.gcode_executor import GCodeExecutor + from .char_counting_queue import CharCountingQueue from .ftpsclient import IoTFTPSClient # noinspection PyBroadException class BambuVirtualPrinter: + gcode_executor = GCodeExecutor() command_regex = re.compile(r"^([GM])(\d+)") def __init__( @@ -68,7 +72,6 @@ class BambuVirtualPrinter: self.current_line = 0 self._writingToSd = False - self._sdCardReady = True self._sdPrinter = None self._sdPrinting = False self._sdPrintStarting = False @@ -89,19 +92,17 @@ class BambuVirtualPrinter: self._faked_baudrate = faked_baudrate self._plugin_data_folder = data_folder - self._seriallog = logging.getLogger( + self._serial_log = logging.getLogger( "octoprint.plugins.bambu_printer.BambuPrinter.serial" ) - self._seriallog.setLevel(logging.CRITICAL) - self._seriallog.propagate = False + self._serial_log.setLevel(logging.CRITICAL) + self._serial_log.propagate = False if seriallog_handler is not None: - import logging.handlers + self._serial_log.addHandler(seriallog_handler) + self._serial_log.setLevel(logging.INFO) - self._seriallog.addHandler(seriallog_handler) - self._seriallog.setLevel(logging.INFO) - - self._seriallog.debug("-" * 78) + self._serial_log.debug("-" * 78) self._read_timeout = read_timeout self._write_timeout = write_timeout @@ -242,7 +243,7 @@ class BambuVirtualPrinter: self._logger.debug( f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}" ) - self.bambu = BambuClient( + self._bambu = BambuClient( device_type=self._settings.get(["device_type"]), serial=self._settings.get(["serial"]), host=self._settings.get(["host"]), @@ -257,10 +258,10 @@ class BambuVirtualPrinter: email=self._settings.get(["email"]), auth_token=self._settings.get(["auth_token"]), ) - self.bambu.on_disconnect = self.on_disconnect(self.bambu.on_disconnect) - self.bambu.on_connect = self.on_connect(self.bambu.on_connect) - self.bambu.connect(callback=self.new_update) - self._logger.info(f"bambu connection status: {self.bambu.connected}") + self._bambu.on_disconnect = self.on_disconnect(self._bambu.on_disconnect) + self._bambu.on_connect = self.on_connect(self._bambu.on_connect) + self._bambu.connect(callback=self.new_update) + self._logger.info(f"bambu connection status: {self._bambu.connected}") self._sendOk() def __str__(self): @@ -289,7 +290,6 @@ class BambuVirtualPrinter: self._feedrate_multiplier = 100 self._flowrate_multiplier = 100 - self._sdCardReady = True self._sdPrinting = False self._sdPrintStarting = False if self._sdPrinter: @@ -460,97 +460,51 @@ class BambuVirtualPrinter: command = command_match.group(0) letter = command_match.group(1) - try: - # if we have a method _gcode_G, _gcode_M or _gcode_T, execute that first - letter_handler = f"_gcode_{letter}" - if hasattr(self, letter_handler): - code = command_match.group(2) - handled = getattr(self, letter_handler)(code, data) - if handled: - self._sendOk() - continue + if letter in self.gcode_executor: + handled = self.run_gcode_handler(letter, data) + else: + handled = self.run_gcode_handler(command, data) + if handled: + self._sendOk() + continue - # then look for a method _gcode_ and execute that if it exists - command_handler = f"_gcode_{command}" - if hasattr(self, command_handler): - handled = getattr(self, command_handler)(data) - if handled: - self._sendOk() - continue - else: + if self.bambu.connected: + GCODE_COMMAND = commands.SEND_GCODE_TEMPLATE + GCODE_COMMAND["print"]["param"] = data + "\n" + if self.bambu.publish(GCODE_COMMAND): + self._logger.info("command sent successfully") self._sendOk() - - if self.bambu.connected: - GCODE_COMMAND = commands.SEND_GCODE_TEMPLATE - GCODE_COMMAND["print"]["param"] = data + "\n" - if self.bambu.publish(GCODE_COMMAND): - self._logger.info("command sent successfully") - self._sendOk() - continue - - finally: - self._logger.debug(f"{data}") + continue + self._logger.debug(f"{data}") self._logger.debug("Closing down read loop") ##~~ command implementations + def run_gcode_handler(self, gcode, data): + self.gcode_executor.execute(self, gcode, data) - # noinspection PyUnusedLocal - def _gcode_M20(self, data: str) -> bool: - if self._sdCardReady: - self._listSd(incl_long="L" in data, incl_timestamp="T" in data) - return True - - # noinspection PyUnusedLocal + @gcode_executor.register("M21") def _gcode_M21(self, data: str) -> bool: - self._sdCardReady = True self._send("SD card ok") return True - # noinspection PyUnusedLocal - def _gcode_M22(self, data: str) -> bool: - self._logger.debug("ignoring M22 command.") - self._send("M22 disabled for Bambu") - return True - + @gcode_executor.register("M23") def _gcode_M23(self, data: str) -> bool: - if self._sdCardReady: - filename = data.split(None, 1)[1].strip() - self._selectSdFile(filename) + filename = data.split(maxsplit=1)[1].strip() + self._selectSdFile(filename) return True - # noinspection PyUnusedLocal - def _gcode_M24(self, data: str) -> bool: - if self._sdCardReady: - self._startSdPrint() - return True - - # noinspection PyUnusedLocal - def _gcode_M25(self, data: str) -> bool: - if self._sdCardReady: - self._pauseSdPrint() - return True - - def _gcode_M524(self, data: str) -> bool: - if self._sdCardReady: - return self._cancelSdPrint() - return False - + @gcode_executor.register("M26") def _gcode_M26(self, data: str) -> bool: if data == "M26 S0": - if self._sdCardReady: - return self._cancelSdPrint() - return False + return self._cancelSdPrint() else: self._logger.debug("ignoring M26 command.") self._send("M26 disabled for Bambu") return True + @gcode_executor.register("M27") def _gcode_M27(self, data: str) -> bool: - def report(): - if self._sdCardReady: - self._reportSdStatus() - matchS = re.search(r"S([0-9]+)", data) if matchS: interval = int(matchS.group(1)) @@ -558,41 +512,26 @@ class BambuVirtualPrinter: self._sdstatus_reporter.cancel() if interval > 0: - self._sdstatus_reporter = RepeatedTimer(interval, report) + self._sdstatus_reporter = RepeatedTimer(interval, self._reportSdStatus) self._sdstatus_reporter.start() else: self._sdstatus_reporter = None - report() - return True - - def _gcode_M28(self, data: str) -> bool: - self._logger.debug("ignoring M28 command.") - self._send("M28 disabled for Bambu") - return True - - # noinspection PyUnusedLocal - def _gcode_M29(self, data: str) -> bool: - self._logger.debug("ignoring M29 command.") - self._send("M29 disabled for Bambu") + self._reportSdStatus() return True + @gcode_executor.register("M30") def _gcode_M30(self, data: str) -> bool: - if self._sdCardReady: - filename = data.split(None, 1)[1].strip() - self._deleteSdFile(filename) + filename = data.split(None, 1)[1].strip() + self._deleteSdFile(filename) return True - def _gcode_M33(self, data: str) -> bool: - self._logger.debug("ignoring M33 command.") - self._send("M33 disabled for Bambu") - return True - - # noinspection PyUnusedLocal + @gcode_executor.register("M105") def _gcode_M105(self, data: str) -> bool: return self._processTemperatureQuery() # noinspection PyUnusedLocal + @gcode_executor.register("M115") def _gcode_M115(self, data: str) -> bool: self._send("Bambu Printer Integration") self._send("Cap:EXTENDED_M20:1") @@ -600,12 +539,14 @@ class BambuVirtualPrinter: self._send("Cap:LFN_WRITE:1") return True + @gcode_executor.register("M117") def _gcode_M117(self, data: str) -> bool: # we'll just use this to echo a message, to allow playing around with pause triggers result = re.search(r"M117\s+(.*)", data).group(1) self._send(f"echo:{result}") return False + @gcode_executor.register("M118") def _gcode_M118(self, data: str) -> bool: match = re.search(r"M118 (?:(?PA1|E1|Pn[012])\s)?(?P.*)", data) if not match: @@ -624,6 +565,7 @@ class BambuVirtualPrinter: return True # noinspection PyUnusedLocal + @gcode_executor.register("M220") def _gcode_M220(self, data: str) -> bool: if self.bambu.connected: gcode_command = commands.SEND_GCODE_TEMPLATE @@ -650,10 +592,6 @@ class BambuVirtualPrinter: ) return True - # noinspection PyUnusedLocal - def _gcode_M400(self, data: str) -> bool: - return True - @staticmethod def _check_param_letters(letters, data): # Checks if any of the params (letters) are included in data @@ -701,7 +639,8 @@ class BambuVirtualPrinter: request_resend() - def _listSd(self, incl_long=False, incl_timestamp=False): + @gcode_executor.register_no_data("M20") + def _listSd(self): line = '{dosname} {size} {timestamp} "{name}"' self._send("Begin file list") @@ -795,7 +734,7 @@ class BambuVirtualPrinter: file = self._getSdFileData(filename) if file is None: - self._listSd(incl_long=True, incl_timestamp=True) + self._listSd() self._sendOk() file = self._getSdFileData(filename) if file is None: @@ -810,7 +749,8 @@ class BambuVirtualPrinter: self._send(f"File opened: {file['name']} Size: {self._selectedSdFileSize}") self._send("File selected") - def _startSdPrint(self, from_printer: bool = False) -> None: + @gcode_executor.register_no_data("M24") + def _startSdPrint(self, from_printer: bool = False) -> bool: self._logger.debug(f"_startSdPrint: from_printer={from_printer}") if self._selectedSdFile is not None: if self._sdPrinter is None: @@ -820,16 +760,16 @@ class BambuVirtualPrinter: target=self._sdPrintingWorker, kwargs={"from_printer": from_printer} ) self._sdPrinter.start() - # self._sdPrintingSemaphore.set() + if self._sdPrinter is not None: if self.bambu.connected: if self.bambu.publish(commands.RESUME): self._logger.info("print resumed") - # if not self._sdPrintingSemaphore.is_set(): - # self._sdPrintingSemaphore.set() else: self._logger.info("print resume failed") + return True + @gcode_executor.register_no_data("M25") def _pauseSdPrint(self): if self.bambu.connected: if self.bambu.publish(commands.PAUSE): @@ -837,6 +777,7 @@ class BambuVirtualPrinter: else: self._logger.info("print pause failed") + @gcode_executor.register("M524") def _cancelSdPrint(self) -> bool: if self.bambu.connected: if self.bambu.publish(commands.STOP): @@ -846,6 +787,7 @@ class BambuVirtualPrinter: else: self._logger.info("print cancel failed") return False + return False def _setSdPos(self, pos): self._newSdFilePos = pos @@ -1003,20 +945,6 @@ class BambuVirtualPrinter: def _setUnbusy(self): self._busy = None - # def _processBuffer(self): - # while self.buffered is not None: - # try: - # line = self.buffered.get(timeout=0.5) - # except queue.Empty: - # continue - # - # if line is None: - # continue - # - # self.buffered.task_done() - # - # self._logger.debug("Closing down buffer loop") - def _showPrompt(self, text, choices): self._hidePrompt() self._send(f"//action:prompt_begin {text}") @@ -1036,7 +964,7 @@ class BambuVirtualPrinter: return 0 if b"M112" in data: - self._seriallog.debug(f"<<< {u_data}") + self._serial_log.debug(f"<<< {u_data}") self._kill() return len(data) @@ -1044,7 +972,7 @@ class BambuVirtualPrinter: written = self.incoming.put( data, timeout=self._write_timeout, partial=True ) - self._seriallog.debug(f"<<< {u_data}") + self._serial_log.debug(f"<<< {u_data}") return written except queue.Full: self._logger.info( @@ -1053,12 +981,13 @@ class BambuVirtualPrinter: raise SerialTimeoutException() def readline(self) -> bytes: + assert self.outgoing is not None timeout = self._read_timeout try: # fetch a line from the queue, wait no longer than timeout line = to_unicode(self.outgoing.get(timeout=timeout), errors="replace") - self._seriallog.debug(f">>> {line.strip()}") + self._serial_log.debug(f">>> {line.strip()}") self.outgoing.task_done() return to_bytes(line) except queue.Empty: @@ -1076,9 +1005,7 @@ class BambuVirtualPrinter: def _sendOk(self): if self.outgoing is None: return - ok = self._ok() - if ok: - self._send(ok) + self._send("ok") def _isPaused(self): return self._sdPrintingPausedSemaphore.is_set() @@ -1100,8 +1027,5 @@ class BambuVirtualPrinter: if self.outgoing is not None: self.outgoing.put(line) - def _ok(self): - return "ok" - def _error(self, error: str, *args, **kwargs) -> str: return f"Error: {self._errors.get(error).format(*args, **kwargs)}" diff --git a/octoprint_bambu_printer/gcode_executor.py b/octoprint_bambu_printer/gcode_executor.py new file mode 100644 index 0000000..20b209d --- /dev/null +++ b/octoprint_bambu_printer/gcode_executor.py @@ -0,0 +1,314 @@ +import itertools +import logging +from inspect import signature + + +GCODE_DOCUMENTATION = { + "G0": "Linear Move", + "G1": "Linear Move", + "G2": "Arc or Circle Move", + "G3": "Arc or Circle Move", + "G4": "Dwell", + "G5": "Bézier cubic spline", + "G6": "Direct Stepper Move", + "G10": "Retract", + "G11": "Recover", + "G12": "Clean the Nozzle", + "G17": "CNC Workspace Planes", + "G18": "CNC Workspace Planes", + "G19": "CNC Workspace Planes", + "G20": "Inch Units", + "G21": "Millimeter Units", + "G26": "Mesh Validation Pattern", + "G27": "Park toolhead", + "G28": "Auto Home", + "G29": "Bed Leveling", + "G29": "Bed Leveling (3-Point)", + "G29": "Bed Leveling (Linear)", + "G29": "Bed Leveling (Manual)", + "G29": "Bed Leveling (Bilinear)", + "G29": "Bed Leveling (Unified)", + "G30": "Single Z-Probe", + "G31": "Dock Sled", + "G32": "Undock Sled", + "G33": "Delta Auto Calibration", + "G34": "Z Steppers Auto-Alignment", + "G34": "Mechanical Gantry Calibration", + "G35": "Tramming Assistant", + "G38.2": "Probe target", + "G38.3": "Probe target", + "G38.4": "Probe target", + "G38.5": "Probe target", + "G42": "Move to mesh coordinate", + "G53": "Move in Machine Coordinates", + "G60": "Save Current Position", + "G61": "Return to Saved Position", + "G76": "Probe temperature calibration", + "G80": "Cancel Current Motion Mode", + "G90": "Absolute Positioning", + "G91": "Relative Positioning", + "G92": "Set Position", + "G425": "Backlash Calibration", + "M0": "Unconditional stop", + "M1": "Unconditional stop", + "M3": "Spindle CW / Laser On", + "M4": "Spindle CCW / Laser On", + "M5": "Spindle / Laser Off", + "M7": "Coolant Controls", + "M8": "Coolant Controls", + "M9": "Coolant Controls", + "M10": "Vacuum / Blower Control", + "M11": "Vacuum / Blower Control", + "M16": "Expected Printer Check", + "M17": "Enable Steppers", + "M18": "Disable steppers", + "M84": "Disable steppers", + "M20": "List SD Card", + "M21": "Init SD card", + "M22": "Release SD card", + "M23": "Select SD file", + "M24": "Start or Resume SD print", + "M25": "Pause SD print", + "M26": "Set SD position", + "M27": "Report SD print status", + "M28": "Start SD write", + "M29": "Stop SD write", + "M30": "Delete SD file", + "M31": "Print time", + "M32": "Select and Start", + "M33": "Get Long Path", + "M34": "SDCard Sorting", + "M42": "Set Pin State", + "M43": "Debug Pins", + "M48": "Probe Repeatability Test", + "M73": "Set Print Progress", + "M75": "Start Print Job Timer", + "M76": "Pause Print Job Timer", + "M77": "Stop Print Job Timer", + "M78": "Print Job Stats", + "M80": "Power On", + "M81": "Power Off", + "M82": "E Absolute", + "M83": "E Relative", + "M85": "Inactivity Shutdown", + "M86": "Hotend Idle Timeout", + "M87": "Disable Hotend Idle Timeout", + "M92": "Set Axis Steps-per-unit", + "M100": "Free Memory", + "M102": "Configure Bed Distance Sensor", + "M104": "Set Hotend Temperature", + "M105": "Report Temperatures", + "M106": "Set Fan Speed", + "M107": "Fan Off", + "M108": "Break and Continue", + "M109": "Wait for Hotend Temperature", + "M110": "Set / Get Line Number", + "M111": "Debug Level", + "M112": "Full Shutdown", + "M113": "Host Keepalive", + "M114": "Get Current Position", + "M115": "Firmware Info", + "M117": "Set LCD Message", + "M118": "Serial print", + "M119": "Endstop States", + "M120": "Enable Endstops", + "M121": "Disable Endstops", + "M122": "TMC Debugging", + "M123": "Fan Tachometers", + "M125": "Park Head", + "M126": "Baricuda 1 Open", + "M127": "Baricuda 1 Close", + "M128": "Baricuda 2 Open", + "M129": "Baricuda 2 Close", + "M140": "Set Bed Temperature", + "M141": "Set Chamber Temperature", + "M143": "Set Laser Cooler Temperature", + "M145": "Set Material Preset", + "M149": "Set Temperature Units", + "M150": "Set RGB(W) Color", + "M154": "Position Auto-Report", + "M155": "Temperature Auto-Report", + "M163": "Set Mix Factor", + "M164": "Save Mix", + "M165": "Set Mix", + "M166": "Gradient Mix", + "M190": "Wait for Bed Temperature", + "M191": "Wait for Chamber Temperature", + "M192": "Wait for Probe temperature", + "M193": "Set Laser Cooler Temperature", + "M200": "Set Filament Diameter", + "M201": "Print / Travel Move Limits", + "M203": "Set Max Feedrate", + "M204": "Set Starting Acceleration", + "M205": "Set Advanced Settings", + "M206": "Set Home Offsets", + "M207": "Set Firmware Retraction", + "M208": "Firmware Recover", + "M209": "Set Auto Retract", + "M211": "Software Endstops", + "M217": "Filament swap parameters", + "M218": "Set Hotend Offset", + "M220": "Set Feedrate Percentage", + "M221": "Set Flow Percentage", + "M226": "Wait for Pin State", + "M240": "Trigger Camera", + "M250": "LCD Contrast", + "M255": "LCD Sleep/Backlight Timeout", + "M256": "LCD Brightness", + "M260": "I2C Send", + "M261": "I2C Request", + "M280": "Servo Position", + "M281": "Edit Servo Angles", + "M282": "Detach Servo", + "M290": "Babystep", + "M300": "Play Tone", + "M301": "Set Hotend PID", + "M302": "Cold Extrude", + "M303": "PID autotune", + "M304": "Set Bed PID", + "M305": "User Thermistor Parameters", + "M306": "Model Predictive Temp. Control", + "M350": "Set micro-stepping", + "M351": "Set Microstep Pins", + "M355": "Case Light Control", + "M360": "SCARA Theta A", + "M361": "SCARA Theta-B", + "M362": "SCARA Psi-A", + "M363": "SCARA Psi-B", + "M364": "SCARA Psi-C", + "M380": "Activate Solenoid", + "M381": "Deactivate Solenoids", + "M400": "Finish Moves", + "M401": "Deploy Probe", + "M402": "Stow Probe", + "M403": "MMU2 Filament Type", + "M404": "Set Filament Diameter", + "M405": "Filament Width Sensor On", + "M406": "Filament Width Sensor Off", + "M407": "Filament Width", + "M410": "Quickstop", + "M412": "Filament Runout", + "M413": "Power-loss Recovery", + "M420": "Bed Leveling State", + "M421": "Set Mesh Value", + "M422": "Set Z Motor XY", + "M423": "X Twist Compensation", + "M425": "Backlash compensation", + "M428": "Home Offsets Here", + "M430": "Power Monitor", + "M486": "Cancel Objects", + "M493": "Fixed-Time Motion", + "M500": "Save Settings", + "M501": "Restore Settings", + "M502": "Factory Reset", + "M503": "Report Settings", + "M504": "Validate EEPROM contents", + "M510": "Lock Machine", + "M511": "Unlock Machine", + "M512": "Set Passcode", + "M524": "Abort SD print", + "M540": "Endstops Abort SD", + "M569": "Set TMC stepping mode", + "M575": "Serial baud rate", + "M592": "Nonlinear Extrusion Control", + "M593": "ZV Input Shaping", + "M600": "Filament Change", + "M603": "Configure Filament Change", + "M605": "Multi Nozzle Mode", + "M665": "Delta Configuration", + "M665": "SCARA Configuration", + "M666": "Set Delta endstop adjustments", + "M666": "Set dual endstop offsets", + "M672": "Duet Smart Effector sensitivity", + "M701": "Load filament", + "M702": "Unload filament", + "M710": "Controller Fan settings", + "M808": "Repeat Marker", + "M851": "XYZ Probe Offset", + "M852": "Bed Skew Compensation", + "M871": "Probe temperature config", + "M876": "Handle Prompt Response", + "M900": "Linear Advance Factor", + "M906": "Stepper Motor Current", + "M907": "Set Motor Current", + "M908": "Set Trimpot Pins", + "M909": "DAC Print Values", + "M910": "Commit DAC to EEPROM", + "M911": "TMC OT Pre-Warn Condition", + "M912": "Clear TMC OT Pre-Warn", + "M913": "Set Hybrid Threshold Speed", + "M914": "TMC Bump Sensitivity", + "M915": "TMC Z axis calibration", + "M916": "L6474 Thermal Warning Test", + "M917": "L6474 Overcurrent Warning Test", + "M918": "L6474 Speed Warning Test", + "M919": "TMC Chopper Timing", + "M928": "Start SD Logging", + "M951": "Magnetic Parking Extruder", + "M993": "Back up flash settings to SD", + "M994": "Restore flash from SD", + "M995": "Touch Screen Calibration", + "M997": "Firmware update", + "M999": "STOP Restart", + "M7219": "MAX7219 Control", +} + + +class GCodeExecutor: + def __init__(self): + self._log = logging.getLogger( + "octoprint.plugins.bambu_printer.BambuPrinter.gcode_executor" + ) + self.handler_names = set() + self.gcode_handlers = {} + self.gcode_handlers_no_data = {} + + def __contains__(self, item): + return item in self.gcode_handlers or item in self.gcode_handlers_no_data + + def _get_required_args_count(self, func): + sig = signature(func) + required_count = sum( + 1 + for p in sig.parameters.values() + if (p.kind == p.POSITIONAL_OR_KEYWORD or p.kind == p.POSITIONAL_ONLY) + and p.default == p.empty + ) + return required_count + + def register(self, gcode): + def decorator(func): + required_count = self._get_required_args_count(func) + if required_count == 1: + self.gcode_handlers_no_data[gcode] = func + elif required_count == 2: + self.gcode_handlers[gcode] = func + else: + raise ValueError( + f"Cannot register function with {required_count} required parameters" + ) + return func + + return decorator + + def register_no_data(self, gcode): + def decorator(func): + self.gcode_handlers_no_data[gcode] = func + return func + + return decorator + + def execute(self, printer, gcode, data): + gcode_info = self._gcode_with_info(gcode) + if gcode in self.gcode_handlers: + self._log.debug(f"Executing {gcode_info}") + return self.gcode_handlers[gcode](printer, data) + elif gcode in self.gcode_handlers_no_data: + self._log.debug(f"Executing {gcode_info}") + return self.gcode_handlers_no_data[gcode](printer) + else: + self._log.debug(f"ignoring {gcode_info} command.") + return True + + def _gcode_with_info(self, gcode): + return f"{gcode} ({GCODE_DOCUMENTATION.get(gcode, 'Info not specified')})"