diff --git a/ChangeLog b/ChangeLog index 5494bac9..a236f565 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,10 +1,6 @@ # ChangeLog -0.7.2 (2023-10-11) - * Add an automatic PrusaLink image builder script - * Add multi-instance documentation - * Telemetry improvement - * Attempt to turn the RPi wifi power management off in the images +0.7.3dev * Stop showing the tune menu after connecting to the printer * Use the 2023-10-10 Raspberry Pi OS base image for the image builder * Decrease the severity of web server access log messages @@ -14,6 +10,13 @@ * Don't send over serial when temperature calibration is running * Periodically send a keepalive gcode to keep the printer in PrusaLink mode * Support the set ready and cancel ready LCD menu toggle + * Add the initial support for the MMU + +0.7.2 (2023-10-11) + * Add an automatic PrusaLink image builder script + * Add multi-instance documentation + * Telemetry improvement + * Attempt to turn the RPi wifi power management off in the images 0.7.1rc1 (2023-08-10) * Fixed HTTP response status on HEAD request for non-existing files diff --git a/prusa/link/const.py b/prusa/link/const.py index d9396016..8370dbde 100644 --- a/prusa/link/const.py +++ b/prusa/link/const.py @@ -31,6 +31,8 @@ 30302: PrinterType.I3MK3S, } +MMU3_TYPE_CODE = 30302 + PRINTER_CONF_TYPES = bidict({ "MK2.5": PrinterType.I3MK25, "MK2.5S": PrinterType.I3MK25S, @@ -245,3 +247,111 @@ class LimitsMK3S(LimitsFDM): } MMU_SLOTS = 5 + +MMU_PROGRESS_MAP = { + "OK": 0, + + "Engaging idler": 1, + "Disengaging idler": 2, + "Unloading to FINDA": 3, + "Unloading to pulley": 4, + "Feeding to FINDA": 5, + "Feeding to extruder": 6, + "Feeding to nozzle": 7, + "Avoiding grind": 8, + "ERR Disengaging idler": 10, + "ERR Engaging idler": 11, + "ERR Wait for User": 12, + "ERR Internal": 13, + "ERR Help filament": 14, + "ERR TMC failed": 15, + "Selecting fil. slot": 18, + "Preparing blade": 19, + "Pushing filament": 20, + "Performing cut": 21, + "Returning selector": 22, + "Ejecting filament": 24, + "Parking selector": 23, + "Retract from FINDA": 25, + "Homing": 26, + "Moving selector": 27, + "Feeding to FSensor": 28, +} + +MMU_ERROR_MAP = { + 0x8001: 101, # FINDA didn't switch on -> FINDA didn't trigger + 0x8002: 102, # FINDA didn't switch off -> FINDA filament stuck + 0x8003: 103, # Filament sensor didn't switch on -> FSENSOR didn't trigger + 0x8004: 104, # Filament sensor didn't switch off -> FSENSOR filament stuck + 0x800b: 105, # MOVE_PULLEY_FAILED -> MECHANICAL pulley cannot move + 0x8009: 106, # FSensor triggered too early -> MECHANICAL FSENSOR too early + 0x800a: 107, # FINDA flickers -> MECHANICAL inspect FINDA + 0x802a: 108, # LOAD_TO_EXTRUDER_FAILED -> Loading to extruder failed. + # Inspect the filament tip shape. Refine the sensor + # calibration, if needed + + 0x8007 | 0x0080: 115, # Selector homing failed + 0x800b | 0x0080: 116, # Selector move failed + 0x8007 | 0x0100: 125, # Idler homing failed + 0x800b | 0x0100: 126, # Idler move failed + + 0xA000: 201, # TMC_OVER_TEMPERATURE_WARN -> Temperature warning TMC pulley + # too hot + 0xC000: 202, # TMC_OVER_TEMPERATURE_ERROR -> Temperature TMC pulley + # overheat error + + # Temperature errors for the selector driver + 0xA000 | 0x0080: 211, # Temperature warning TMC selector too hot + 0xC000 | 0x0080: 212, # Temperature TMC selector overheat error + + # Temperature errors for the idler driver + 0xA000 | 0x0100: 221, # Temperature warning TMC idler too hot + 0xC000 | 0x0100: 222, # Temperature TMC idler overheat error + + # Electrical errors + 0x8200: 301, # TMC_IOIN_MISMATCH -> Electrical TMC pulley driver error + 0x8400: 302, # TMC_RESET -> Electrical TMC pulley driver reset + 0x8800: 303, # TMC_UNDERVOLTAGE_ON_CHARGE_PUMP -> Electrical TMC + # pulley undervoltage error + 0x9000: 304, # TMC_SHORT_TO_GROUND -> Electrical TMC pulley driver shorted + 0xC200: 305, # ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED -> Electrical + # TMC pulley selftest failed + + # Electrical errors for the selector driver + 0x8200 | 0x0080: 311, # Electrical TMC selector driver error + 0x8400 | 0x0080: 312, # Electrical TMC selector driver reset + 0x8800 | 0x0080: 313, # Electrical TMC selector undervoltage error + 0x9000 | 0x0080: 314, # Electrical TMC selector driver shorted + 0xC200 | 0x0080: 315, # Electrical TMC selector selftest failed + + # Electrical errors for the idler driver + 0x8200 | 0x0100: 321, # Electrical TMC idler driver error + 0x8400 | 0x0100: 322, # Electrical TMC idler driver reset + 0x8800 | 0x0100: 323, # Electrical TMC idler undervoltage error + 0x9000 | 0x0100: 324, # Electrical TMC idler driver shorted + 0xC200 | 0x0100: 325, # Electrical TMC idler selftest failed + + 0x0800d: 306, # MMU MCU detected a 5V undervoltage. There might be an + # issue with the electronics. Check the wiring and + # connectors + + # Connectivity errors + 0x802e: 401, # MMU not responding -> CONNECT MMU not responding + 0x802d: 402, # MMU not responding correctly. Check the wiring and + # connectors + + # System errors + 0x8005: 501, # Filament already loaded -> SYSTEM filament already loaded + 0x8006: 502, # Invalid tool -> SYSTEM invalid tool + 0x802b: 503, # QUEUE_FULL -> MMU Firmware internal error, please reset + # the MMU + 0x802c: 504, # VERSION_MISMATCH -> The MMU firmware version is + # incompatible with the printer's FW. Update to compatible + # version + 0x802f: 505, # PROTOCOL_ERROR -> Internal runtime error. Try resetting + # the MMU or updating the firmware + 0x8008: 506, # FINDA_VS_EEPROM_DISCREPANCY -> Unload manually + 0x800c: 507, # Filament was ejected -> SYSTEM filament ejected + 0x8029: 508, # FILAMENT_CHANGE -> SYSTEM filament change + +} diff --git a/prusa/link/printer_adapter/mmu_observer.py b/prusa/link/printer_adapter/mmu_observer.py new file mode 100644 index 00000000..c5bc7066 --- /dev/null +++ b/prusa/link/printer_adapter/mmu_observer.py @@ -0,0 +1,121 @@ +"""Contains the mmu output observing code, that compiles the readouts into + telemetry values""" +from re import Match + +from blinker import Signal + +from prusa.connect.printer.const import Event, Source + +from ..const import MMU_ERROR_MAP, MMU_PROGRESS_MAP +from ..sdk_augmentation.printer import MyPrinter +from ..serial.serial_parser import ThreadedSerialParser +from .model import Model +from .structures.model_classes import Slot, Telemetry +from .structures.module_data_classes import MMUObserverData +from .structures.regular_expressions import ( + MMU_PROGRESS_REGEX, + MMU_Q0_REGEX, + MMU_Q0_RESPONSE_REGEX, + MMU_SLOT_REGEX, +) +from .telemetry_passer import TelemetryPasser + + +class MMUObserver: + """The class that observes the MMU output and sends passes the info + from it as telemetry""" + + def __init__(self, + serial_parser: ThreadedSerialParser, + model: Model, + printer: MyPrinter, + telemetry_passer: TelemetryPasser): + self.serial_parser = serial_parser + self.model = model + self.model.mmu_observer = MMUObserverData(current_error_code=None) + self.data = self.model.mmu_observer + self.printer = printer + self.telemetry_passer = telemetry_passer + + self.capture_q0 = False + + self.serial_parser.add_decoupled_handler( + MMU_PROGRESS_REGEX, self._handle_mmu_progress) + self.serial_parser.add_decoupled_handler( + MMU_SLOT_REGEX, self._handle_active_slot) + self.serial_parser.add_decoupled_handler( + MMU_Q0_RESPONSE_REGEX, self._handle_q0_response) + self.serial_parser.add_decoupled_handler( + MMU_Q0_REGEX, self._prime_q0) + + self.error_changed_signal = Signal() + + def _prime_q0(self, _, match: Match) -> None: + """Starts listening for the Q0 response""" + assert match is not None + self.capture_q0 = True + + def _handle_mmu_progress(self, _, match: Match): + message = match.group("message") + code = str(MMU_PROGRESS_MAP.get(message)) + self.telemetry_passer.set_telemetry( + Telemetry( + slot=Slot( + state=code, + ), + ), + ) + + def _handle_active_slot(self, _, match: Match): + active_slot = int(match.group("slot"), 16) + self.telemetry_passer.set_telemetry( + Telemetry( + slot=Slot( + active=active_slot, + ), + ), + ) + + def _handle_mmu_error(self, error_code): + """Report an mmu error""" + prusa_error_code = "04" + str(MMU_ERROR_MAP.get(error_code)) + if self.data.current_error_code == prusa_error_code: + return + self.data.current_error_code = prusa_error_code + self.printer.event_cb( + Event.SLOT_EVENT, + source=Source.SLOT, + code=prusa_error_code, + ) + self.error_changed_signal.send() + + def _handle_mmu_no_error(self): + """Clear the mmu error""" + self.data.current_error_code = None + self.error_changed_signal.send() + + def _handle_q0_response(self, _, match: Match): + """Parse the mmu Q0 status response""" + if not self.capture_q0: + return + self.capture_q0 = False + + command_code = match.group("command") + progress_code = match.group("progress") + + # Is there a command in progress? If yes, send it + if progress_code[0] in "PE": + self.telemetry_passer.set_telemetry( + Telemetry( + slot=Slot( + command=command_code, + ), + ), + ) + + # Figure out if there's an error being reported + if progress_code.startswith("E"): + error_code = int(progress_code[1:], 16) + self._handle_mmu_error(error_code) + else: + self._handle_mmu_no_error() diff --git a/prusa/link/printer_adapter/model.py b/prusa/link/printer_adapter/model.py index a90c7d1f..6399240c 100644 --- a/prusa/link/printer_adapter/model.py +++ b/prusa/link/printer_adapter/model.py @@ -6,6 +6,7 @@ FilePrinterData, IPUpdaterData, JobData, + MMUObserverData, PrintStatsData, SDCardData, SerialAdapterData, @@ -34,6 +35,7 @@ class Model(metaclass=MCSingleton): sd_card: SDCardData folder_storage: StorageData filesystem_storage: StorageData + mmu_observer: MMUObserverData def __init__(self) -> None: self.latest_telemetry: Telemetry = Telemetry() diff --git a/prusa/link/printer_adapter/printer_polling.py b/prusa/link/printer_adapter/printer_polling.py index 8beed547..c2241fb3 100644 --- a/prusa/link/printer_adapter/printer_polling.py +++ b/prusa/link/printer_adapter/printer_polling.py @@ -19,6 +19,7 @@ FAST_POLL_INTERVAL, MINIMAL_FIRMWARE, MK25_PRINTERS, + MMU3_TYPE_CODE, PRINT_MODE_ID_PAIRING, PRINT_STATE_PAIRING, PRINTER_TYPES, @@ -51,6 +52,10 @@ FW_REGEX, M27_OUTPUT_REGEX, MBL_REGEX, + MMU_BUILD_REGEX, + MMU_MAJOR_REGEX, + MMU_MINOR_REGEX, + MMU_REVISION_REGEX, NOZZLE_REGEX, PERCENT_REGEX, PRINT_INFO_REGEX, @@ -111,7 +116,9 @@ def __init__(self, serial_queue: SerialQueue, "printer_type", gather_function=self._get_printer_type, write_function=self._set_printer_type, - validation_function=self._validate_printer_type) + validation_function=self._validate_printer_type, + interval=VERY_SLOW_POLL_INTERVAL, + on_fail_interval=SLOW_POLL_INTERVAL) self.printer_type.became_valid_signal.connect( self._printer_type_became_valid) self.printer_type.val_err_timeout_signal.connect( @@ -154,15 +161,30 @@ def __init__(self, serial_queue: SerialQueue, gather_function=self.get_active_sheet, ) + self.mmu_connected = WatchedItem( + "mmu_connected", + ) + self.mmu_connected.became_valid_signal.connect( + self._mmu_connected_became_valid) + + self.mmu_version = WatchedItem( + "mmu_version", + gather_function=self._get_mmu_version, + ) + self.mmu_version.became_valid_signal.connect( + self._printer_info_became_valid) + self.printer_info = InfoGroup([ self.network_info, self.printer_type, self.firmware_version, self.nozzle_diameter, self.serial_number, self.sheet_settings, - self.active_sheet, + self.active_sheet, self.mmu_connected, ]) for item in self.printer_info: self.item_updater.add_item(item, start_tracking=False) + self.item_updater.add_item(self.mmu_version, start_tracking=False) + # TODO: Put this outside for item in self.printer_info: if item.name in {"active_sheet", "sheet_settings"}: @@ -364,6 +386,7 @@ def invalidate_printer_info(self): for item in itertools.chain(self.telemetry, self.other_stuff, self.printer_info): self.item_updater.disable(item) + self.item_updater.disable(self.mmu_version) self.item_updater.enable(self.printer_type) @@ -406,6 +429,7 @@ def polling_not_ok(self): self._change_interval(self.sheet_settings, None) self._change_interval(self.active_sheet, None) self._change_interval(self.flash_air, None) + self._change_interval(self.printer_type, None) def polling_ok(self): """Re-starts polling of some values""" @@ -416,6 +440,7 @@ def polling_ok(self): self._change_interval(self.sheet_settings, VERY_SLOW_POLL_INTERVAL) self._change_interval(self.active_sheet, SLOW_POLL_INTERVAL) self._change_interval(self.flash_air, VERY_SLOW_POLL_INTERVAL) + self._change_interval(self.printer_type, VERY_SLOW_POLL_INTERVAL) def ensure_job_id(self): """This is an oddball, I don't have anything able to ensure the job_id @@ -438,13 +463,14 @@ def should_wait(self): """Gather helper returning if the component is still running""" return self.item_updater.running - def do_matchable(self, gcode, regex, to_front=False): + def do_matchable(self, gcode, regex, to_front=False, has_to_match=True): """Analog to the command one, as the getters do this over and over again""" instruction = enqueue_matchable(self.serial_queue, gcode, regex, - to_front=to_front) + to_front=to_front, + has_to_match=has_to_match) wait_for_instruction(instruction, self.should_wait) match = instruction.match() if match is None: @@ -497,7 +523,10 @@ def _get_printer_type(self): match = self.do_matchable("M862.2 Q", PRINTER_TYPE_REGEX, to_front=True) - return int(match.group("code")) + code = int(match.group("code")) + mmu_connected = code == MMU3_TYPE_CODE + self.item_updater.set_value(self.mmu_connected, mmu_connected) + return code def _get_firmware_version(self): """Try to get firmware version from the printer.""" @@ -565,6 +594,21 @@ def get_active_sheet(self): active_sheet = struct.unpack("B", data)[0] return active_sheet + def _get_mmu_version(self): + """Gets the mmu_version""" + major_match = self.do_matchable( + "M707 A0x00", MMU_MAJOR_REGEX, has_to_match=False) + minor_match = self.do_matchable( + "M707 A0x01", MMU_MINOR_REGEX, has_to_match=False) + revision_match = self.do_matchable( + "M707 A0x02", MMU_REVISION_REGEX, has_to_match=False) + build_match = self.do_matchable( + "M707 A0x03", MMU_BUILD_REGEX, has_to_match=False) + matches = [major_match, minor_match, revision_match, build_match] + numbers = list(map(lambda match: str(int(match.group("number"), 16)), + matches)) + return ".".join(numbers[:-1]) + "+" + numbers[-1] + def _get_job_id(self): """Gets the current job_id from the printer""" match = self.do_matchable( @@ -1005,9 +1049,27 @@ def _firmware_version_became_valid(self, _): self.item_updater.enable(item) self._set_fw_condition(CondState.OK) + def _mmu_connected_became_valid(self, _): + """MMU connected became valid, enable polling of its version""" + if self.mmu_connected.value: + self.item_updater.enable(self.mmu_version) + else: + self.item_updater.set_value(self.mmu_version, None) + self.item_updater.disable(self.mmu_version) + def _printer_info_became_valid(self, _): """Printer info became valid, we can start looking at telemetry - and other stuff""" + and other stuff + + Also activated when the mmu version becomes valid + This only works because the mmu_version cannot become valide unless + the printer_info is valid already + """ + + if self.mmu_connected.value: + if not self.mmu_version.valid: + return # We'll get here again when it becomes valid + self._send_info_if_changed() for item in itertools.chain(self.telemetry, self.other_stuff): self.item_updater.enable(item) diff --git a/prusa/link/printer_adapter/prusa_link.py b/prusa/link/printer_adapter/prusa_link.py index 9561183b..c5c3b437 100644 --- a/prusa/link/printer_adapter/prusa_link.py +++ b/prusa/link/printer_adapter/prusa_link.py @@ -12,9 +12,9 @@ from prusa.connect.printer.camera_configurator import CameraConfigurator from prusa.connect.printer.camera_driver import CameraDriver from prusa.connect.printer.conditions import API, CondState +from prusa.connect.printer.const import MMU_SLOT_COUNTS, MMUType, Source, State from prusa.connect.printer.const import Command as CommandType from prusa.connect.printer.const import Event as EventType -from prusa.connect.printer.const import Source, State from prusa.connect.printer.files import File from prusa.connect.printer.models import Sheet as SDKSheet @@ -69,6 +69,7 @@ from .job import Job, JobState from .keepalive import Keepalive from .lcd_printer import LCDPrinter +from .mmu_observer import MMUObserver from .model import Model from .print_stat_doubler import PrintStatDoubler from .printer_polling import PrinterPolling @@ -97,6 +98,8 @@ log = logging.getLogger(__name__) +# pylint: disable=too-many-lines + class TransferCallbackState(Enum): """Return values form download_finished_cb.""" @@ -314,6 +317,10 @@ def __init__(self, cfg: Config, settings: Settings) -> None: self.sheet_settings_changed) self.printer_polling.active_sheet.value_changed_signal.connect( self.active_sheet_changed) + self.printer_polling.mmu_connected.value_changed_signal.connect( + self.mmu_connection_changed) + self.printer_polling.mmu_version.value_changed_signal.connect( + self.mmu_info_changed) self.printer_polling.speed_multiplier.value_changed_signal.connect( lambda val: self.lcd_printer.notify(), weak=False) @@ -331,6 +338,12 @@ def __init__(self, cfg: Config, settings: Settings) -> None: self.telemetry_passer) self.auto_telemetry.start() + self.mmu_observer = MMUObserver(self.serial_parser, + self.model, self.printer, + self.telemetry_passer) + + self.mmu_observer.error_changed_signal.connect(self.mmu_error_changed) + self.keepalive.start() self.printer_polling.start() self.storage_controller.start() @@ -614,8 +627,6 @@ def mbl_data_changed(self, data) -> None: def sheet_settings_changed(self, printer_sheets: List[Sheet]) -> None: """Sends the new sheet settings""" - if not self.printer.is_initialised(): - return sdk_sheets: List[SDKSheet] = [] sheet: Sheet for sheet in printer_sheets: @@ -624,19 +635,64 @@ def sheet_settings_changed(self, printer_sheets: List[Sheet]) -> None: "z_offset": sheet.z_offset, }) self.printer.sheet_settings = sdk_sheets + + if not self.printer.is_initialised(): + return self.printer.event_cb(event=EventType.INFO, source=Source.USER, sheet_settings=sdk_sheets) def active_sheet_changed(self, active_sheet) -> None: """Sends the new active sheet""" + self.printer.active_sheet = active_sheet + if not self.printer.is_initialised(): return - self.printer.active_sheet = active_sheet self.printer.event_cb(event=EventType.INFO, source=Source.USER, active_sheet=active_sheet) + def mmu_connection_changed(self, _) -> None: + """Notifies the telemetry passer about the new state + of the mmu connection ans continues ba calling the info sending method + """ + self.telemetry_passer.state_changed() + self.mmu_info_changed(_) + + def mmu_info_changed(self, _) -> None: + """Sends the mmu connection status""" + mmu_connected = self.printer_polling.mmu_connected.value + mmu_version = self.printer_polling.mmu_version.value + if not mmu_connected: + mmu_version = None + + self.printer.mmu_enabled = mmu_connected + # Hardcoded MMU3, sorry + self.printer.mmu_type = MMUType.MMU3 if mmu_connected else None + + if not self.printer_polling.mmu_version.valid: + return + + self.printer.mmu_fw = mmu_version + + if not self.printer.is_initialised(): + return + + kwargs: Dict[str, Any] = {} + mmu = {"enabled": mmu_connected} + if mmu_connected and self.printer.mmu_type is not None: + mmu["version"] = mmu_version + kwargs["slots"] = MMU_SLOT_COUNTS[self.printer.mmu_type] + kwargs["mmu"] = mmu + + self.printer.event_cb(event=EventType.INFO, + source=Source.FIRMWARE, + **kwargs) + + def mmu_error_changed(self, _) -> None: + """Connect the mmu error code changing to the state manager""" + self.state_manager.mmu_error_changed() + def job_info_updated(self, _) -> None: """On job info update, sends the updated job info to the Connect""" # pylint: disable=unsupported-assignment-operation,not-a-mapping diff --git a/prusa/link/printer_adapter/state_manager.py b/prusa/link/printer_adapter/state_manager.py index 7845816b..ec3abf21 100644 --- a/prusa/link/printer_adapter/state_manager.py +++ b/prusa/link/printer_adapter/state_manager.py @@ -414,6 +414,25 @@ def fan_error(self, sender, match: re.Match): if state not in {State.PRINTING, State.ERROR}: self.attention() + def mmu_error_changed(self): + """ + If the MMU error has changed, enter attention if the error is not None, + attempt to leave attention otherwise + """ + current_error_code = self.model.mmu_observer.current_error_code + if current_error_code is None: + self.expect_change( + StateChange(to_states={State.ATTENTION: Source.SLOT}, + reason=current_error_code)) + self._clear_attention() + else: + self.expect_change( + StateChange(to_states={State.ATTENTION: Source.SLOT}, + reason=current_error_code)) + self.attention() + self.stop_expecting_change() + + def fan_error_resolver(self, sender, match): """ If the fan speeds are indicative of a fan error being resolved @@ -729,11 +748,16 @@ def _attention_timer_handler(self): def _clear_attention(self): """Clears the ATTENTION state, if the conditions are right""" - if (self.data.override_state == State.ATTENTION - and self.fan_error_name is None): - log.debug("Clearing ATTENTION") - self.data.override_state = None - self.stop_attention_timer() + if self.data.override_state != State.ATTENTION: + return + if self.fan_error_name is not None: + return + if self.model.mmu_observer.current_error_code is not None: + return + + log.debug("Clearing ATTENTION") + self.data.override_state = None + self.stop_attention_timer() @state_influencer(StateChange(from_states={State.ATTENTION: Source.USER})) def clear_attention(self): diff --git a/prusa/link/printer_adapter/structures/model_classes.py b/prusa/link/printer_adapter/structures/model_classes.py index fd7c9dd4..f7a77902 100644 --- a/prusa/link/printer_adapter/structures/model_classes.py +++ b/prusa/link/printer_adapter/structures/model_classes.py @@ -78,6 +78,7 @@ def dict(self, **kwargs) -> Dict: return data + class NetworkInfo(BaseModel): """The Network Info model""" diff --git a/prusa/link/printer_adapter/structures/module_data_classes.py b/prusa/link/printer_adapter/structures/module_data_classes.py index 61a4c1a3..a096803a 100644 --- a/prusa/link/printer_adapter/structures/module_data_classes.py +++ b/prusa/link/printer_adapter/structures/module_data_classes.py @@ -129,6 +129,11 @@ class StorageData(BaseModel): attached_set: Set[str] +class MMUObserverData(BaseModel): + """Data of the MMUObserver""" + current_error_code: Optional[str] + + class PrintStatsData(BaseModel): """Data of the PrintStats class""" print_time: float diff --git a/prusa/link/printer_adapter/structures/regular_expressions.py b/prusa/link/printer_adapter/structures/regular_expressions.py index 235da9d8..4b4a7fe9 100644 --- a/prusa/link/printer_adapter/structures/regular_expressions.py +++ b/prusa/link/printer_adapter/structures/regular_expressions.py @@ -1,6 +1,8 @@ """Contains every regular expression used in the app as a constant""" import re +from ...const import MMU_PROGRESS_MAP + OPEN_RESULT_REGEX = re.compile( r"^((?PFile opened): (?P.*) Size: (?P\d+))" r"|(?Popen failed).*") @@ -151,3 +153,26 @@ TM_CAL_START_REGEX = re.compile(r"^TM: calibration start$") TM_CAL_END_REGEX = re.compile(r"^(TM: calibr\. failed!)|" r"(Thermal Model settings:)$") + +MMU_MAJOR_REGEX = re.compile( + r"^echo:MMU[23]:[0-9a-fA-F]+)\*..\.$") +MMU_MINOR_REGEX = re.compile( + r"^echo:MMU[23]:[0-9a-fA-F]+)\*..\.$") +MMU_REVISION_REGEX = re.compile( + r"^echo:MMU[23]:[0-9a-fA-F]+)\*..\.$") +MMU_BUILD_REGEX = re.compile( + r"^echo:MMU[23]:[0-9a-fA-F]+)\*..\.$") +MMU_SLOT_REGEX = re.compile( + r"^echo:MMU[23]:[0-9a-fA-F])\*..\.$") +# This can report an error or a command in progress, +# we don't know before parsing +MMU_Q0_RESPONSE_REGEX = re.compile( + r"^echo:MMU[23]:<(?P[A-Z][0-9a-fA-F]+) " + r"(?P[EFP]([0-9a-fA-F]{0,4}))\*..\.$") +MMU_Q0_REGEX = re.compile(r"^echo:MMU[23]:>Q0\*..\.$") + +MMU_PROGRESS_REGEX = re.compile( + r"echo:MMU2:(?P" + + r"|".join(map(re.escape, MMU_PROGRESS_MAP.keys())) + + r")" +) diff --git a/prusa/link/printer_adapter/telemetry_passer.py b/prusa/link/printer_adapter/telemetry_passer.py index 4d5d991e..903d927a 100644 --- a/prusa/link/printer_adapter/telemetry_passer.py +++ b/prusa/link/printer_adapter/telemetry_passer.py @@ -43,6 +43,7 @@ class Modifier(Enum): """The modifiers for telemetry""" FILTER_IDLE = "FILTER_IDLE" # Filtered when idle FILTER_PRINTING = "FILTER_PRINTING" # Filtered when printing + FILTER_MMU_OFF = "FILTER_MMU_OFF" # Filtered when MMU is disconnected JITTER_TEMP = "JITTER_TEMP" # Temperature jitter filtr preset ACTIVATE_IDLE = "ACTIVATE_IDLE" # Wakes up fast telemetry when idle ACTIVATE_PRINTING = "ACTIVATE_PRINTING" # Same but when printing @@ -64,6 +65,7 @@ class Modifier(Enum): ("time_remaining",): {Modifier.FILTER_IDLE}, ("progress",): {Modifier.FILTER_IDLE}, ("inaccurate_estimates",): {Modifier.FILTER_IDLE}, + ("slot",): {Modifier.FILTER_MMU_OFF}, # ("a", "b") - applies to a key b in a subtree a # ("a") - applies to "a", so if it's filtered, its children are too } @@ -375,11 +377,20 @@ def state_changed(self): with self.lock: # Update the active filters state = self.model.state_manager.current_state - self._active_filters.clear() if state not in PRINTING_STATES: self._active_filters.add(Modifier.FILTER_IDLE) - elif state == State.PRINTING: + elif Modifier.FILTER_IDLE in self._active_filters: + self._active_filters.remove(Modifier.FILTER_IDLE) + + if state == State.PRINTING: self._active_filters.add(Modifier.FILTER_PRINTING) + elif Modifier.FILTER_PRINTING in self._active_filters: + self._active_filters.remove(Modifier.FILTER_PRINTING) + + if not self.printer.mmu_enabled: + self._active_filters.add(Modifier.FILTER_MMU_OFF) + elif Modifier.FILTER_MMU_OFF in self._active_filters: + self._active_filters.remove(Modifier.FILTER_MMU_OFF) # Update the telemetry to reflect new filters self.set_telemetry(self._latest_full) diff --git a/prusa/link/serial/helpers.py b/prusa/link/serial/helpers.py index ffbff898..69bfef20 100644 --- a/prusa/link/serial/helpers.py +++ b/prusa/link/serial/helpers.py @@ -1,7 +1,7 @@ """Contains helper functions, for instruction enqueuing""" import re from threading import Event -from typing import Callable, List +from typing import Callable, List, Union from ..const import QUIT_INTERVAL from ..serial.instruction import ( @@ -48,11 +48,15 @@ def enqueue_instruction(queue: SerialQueue, return instruction +# pylint: disable=too-many-arguments def enqueue_matchable(queue: SerialQueue, message: str, regexp: re.Pattern, to_front=False, - to_checksum=False) -> MandatoryMatchableInstruction: + to_checksum=False, + has_to_match=True) -> Union[ + MandatoryMatchableInstruction, + MatchableInstruction]: """ Creates a matchable instruction, which it enqueues right away :param queue: the queue to enqueue into @@ -64,9 +68,15 @@ def enqueue_matchable(queue: SerialQueue, only for print instructions!) :return the enqueued instruction """ - instruction = MandatoryMatchableInstruction(message, - capture_matching=regexp, - to_checksum=to_checksum) + instruction: Union[MandatoryMatchableInstruction, MatchableInstruction] + if has_to_match: + instruction = MandatoryMatchableInstruction(message, + capture_matching=regexp, + to_checksum=to_checksum) + else: + instruction = MatchableInstruction(message, + capture_matching=regexp, + to_checksum=to_checksum) queue.enqueue_one(instruction, to_front=to_front) return instruction diff --git a/prusa/link/util.py b/prusa/link/util.py index bee1f7f8..10acd82a 100644 --- a/prusa/link/util.py +++ b/prusa/link/util.py @@ -16,8 +16,9 @@ import pyudev # type: ignore import unidecode -from .const import SD_STORAGE_NAME, SUPPORTED_PRINTERS +from .const import MMU_SLOTS, SD_STORAGE_NAME, SUPPORTED_PRINTERS from .multi_instance.const import VALID_SN_REGEX +from .printer_adapter.structures.model_classes import IndividualSlot, Slot log = logging.getLogger(__name__) @@ -285,3 +286,22 @@ def walk_dict(data: dict, key_path=None): yield from walk_dict(value, key_path + [key]) else: yield key_path + [key], value + + +def slots_with_param(model, key, default, value): + """Fills out the slot information with defaults, only the active one gets + the real value""" + slot: Slot = model.latest_telemetry.slot + if slot is None: + return None + active_slot = slot.active + + slots = {} + for slot in range(1, MMU_SLOTS + 1): + slot_name = str(slot) + slots[slot_name] = IndividualSlot() + if slot == active_slot: + setattr(slots[slot_name], key, value) + else: + setattr(slots[slot_name], key, default) + return slots