From b2c0cb46b22cc59c43092272646b840f90798033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Joz=C3=ADfek?= Date: Thu, 30 Nov 2023 15:01:01 +0100 Subject: [PATCH] Add host power panic recovery --- .../link/printer_adapter/command_handlers.py | 36 ++- prusa/link/printer_adapter/file_printer.py | 213 ++++++++++++------ prusa/link/printer_adapter/lcd_printer.py | 19 +- prusa/link/printer_adapter/printer_polling.py | 6 +- prusa/link/printer_adapter/prusa_link.py | 51 ++++- prusa/link/printer_adapter/state_manager.py | 7 +- .../structures/model_classes.py | 9 + .../structures/module_data_classes.py | 5 +- .../structures/regular_expressions.py | 1 + prusa/link/serial/serial_adapter.py | 26 ++- prusa/link/serial/serial_queue.py | 11 + prusa/link/util.py | 8 + 12 files changed, 307 insertions(+), 85 deletions(-) diff --git a/prusa/link/printer_adapter/command_handlers.py b/prusa/link/printer_adapter/command_handlers.py index b7835011..4b0101bc 100644 --- a/prusa/link/printer_adapter/command_handlers.py +++ b/prusa/link/printer_adapter/command_handlers.py @@ -25,12 +25,18 @@ STATE_CHANGE_TIMEOUT, ) from ..serial.helpers import enqueue_instruction, enqueue_list_from_str -from ..util import file_is_on_sd, round_to_five +from ..util import ( + _parse_little_endian_uint32, + file_is_on_sd, + get_d3_code, + round_to_five, +) from .command import Command, CommandFailed, FileNotFound, NotStateToPrint from .model import Model from .state_manager import StateChange -from .structures.model_classes import JobState +from .structures.model_classes import EEPROMParams, JobState from .structures.regular_expressions import ( + D3_OUTPUT_REGEX, OPEN_RESULT_REGEX, PRINTER_BOOT_REGEX, REJECTION_REGEX, @@ -663,3 +669,29 @@ def _run_command(self): """Enables resets""" change_reset_mode(self.model, self.serial_adapter, self.serial_parser, self.quit_evt, timeout=self.timeout, enable=True) + + +class PPRecovery(Command): + """Class for recovering from the host power panic""" + command_name = "pp_recovery" + + def _run_command(self): + """Recovers from host power panic""" + if self.model.file_printer.recovering: + return + try: + if not self.file_printer.pp_exists: + raise CommandFailed("No PP file exists, cannot recover.") + except CommandFailed as exception: + enqueue_instruction(self.serial_queue, "M117 \x7ERecovery failed", + to_front=True) + enqueue_instruction(self.serial_queue, "M603", to_front=True) + raise exception + + d_code = get_d3_code(*EEPROMParams.EEPROM_FILE_POSITION.value) + match = self.do_matchable(d_code, D3_OUTPUT_REGEX).match() + if match is None: + raise CommandFailed("Failed to get file position") + line_number = _parse_little_endian_uint32(match) + self.file_printer.recover_from_pp(line_number) + self.serial_queue.set_message_number(line_number) diff --git a/prusa/link/printer_adapter/file_printer.py b/prusa/link/printer_adapter/file_printer.py index 4e190546..84380039 100644 --- a/prusa/link/printer_adapter/file_printer.py +++ b/prusa/link/printer_adapter/file_printer.py @@ -1,7 +1,9 @@ """Contains implementation of the FilePrinter class""" +import json import logging import os from collections import deque +from threading import RLock from time import sleep from typing import Optional @@ -17,10 +19,10 @@ from .model import Model from .print_stats import PrintStats from .structures.mc_singleton import MCSingleton +from .structures.model_classes import PPData from .structures.module_data_classes import FilePrinterData from .structures.regular_expressions import ( CANCEL_REGEX, - POWER_PANIC_REGEX, RESUMED_REGEX, ) from .updatable import Thread @@ -52,10 +54,15 @@ def __init__(self, serial_queue: SerialQueue, # total: int self.layer_trigger_signal = Signal() + self.lock = RLock() + self.model.file_printer = FilePrinterData( printing=False, paused=False, - stopped_forcefully=False, + recovering=False, + was_stopped=False, + power_panic=False, + recovery_ready=False, file_path="", pp_file_path=get_clean_path(cfg.daemon.power_panic_file), enqueued=deque(), @@ -63,8 +70,6 @@ def __init__(self, serial_queue: SerialQueue, gcode_number=0) self.data = self.model.file_printer - self.serial_parser.add_decoupled_handler( - POWER_PANIC_REGEX, lambda sender, match: self.power_panic()) self.serial_parser.add_decoupled_handler( CANCEL_REGEX, lambda sender, match: self.stop_print()) self.serial_parser.add_decoupled_handler( @@ -91,45 +96,62 @@ def pp_exists(self) -> bool: """Checks whether a file created on power panic exists""" return os.path.exists(self.data.pp_file_path) - def check_failed_print(self) -> None: - """Not implemented, would try to resume after power panic or error""" - # log.warning("There was a loss of power, let's try to recover") - if self.pp_exists: - os.remove(self.data.pp_file_path) - - def print(self, os_path: str) -> None: + def print(self, os_path: str, from_gcode_number=None) -> None: """Starts a file print for the supplied path""" if self.data.printing: raise RuntimeError("Cannot print two things at once") + if from_gcode_number is None and self.pp_exists: + os.remove(self.data.pp_file_path) + self.data.file_path = os_path self.thread = Thread(target=self._print, name="file_print", + args=(from_gcode_number,), daemon=True) self.data.printing = True - self.data.stopped_forcefully = False + self.data.was_stopped = False + self.data.power_panic = False + self.data.paused = False + self.data.enqueued.clear() self.print_stats.start_time_segment() self.new_print_started_signal.send(self) self.print_stats.track_new_print(self.data.file_path) self.thread.start() - def _print(self, from_line=0): + def power_panic(self) -> None: + """Handle the printer sending us a power panic signal + This means halt the serial print, do not send any more instructions + Do not delete the power panic file""" + self.data.power_panic = True + self.data.printing = False + log.warning("Power panic!") + + def _print(self, from_gcode_number=None): """ Parses and sends the gcode commands from the file to serial. Supports pausing, resuming and stopping. + + param from_gcode_number: + the gcode number to start from. Implies power panic recovery - + goes into pause when the correct gcode number is reached """ + self.data.recovering = from_gcode_number is not None prctl_name() total_size = os.path.getsize(self.data.file_path) - with open(self.data.file_path, "r", encoding='utf-8') as file: + with (open(self.data.file_path, "r", encoding='utf-8') as file): # Reset the line counter, printing a new file - self.serial_queue.reset_message_number() + if not self.data.recovering: + self.serial_queue.reset_message_number() self.data.gcode_number = 0 self.data.enqueued.clear() line_index = 0 - self.do_instruction("M75") # start printer's print timer + if not self.data.recovering: + self.do_instruction("M75") # start printer's print timer + while True: line = file.readline() @@ -137,6 +159,19 @@ def _print(self, from_line=0): if line == "": break + self.data.line_number = line_index + 1 + gcode = get_gcode(line) + # Skip to the part we need to recover from + if (self.data.recovering + and from_gcode_number > self.data.gcode_number): + if gcode: + self.data.gcode_number += 1 + continue + + # Skip finished, pause here, remove the recovering flag + if self.data.recovering: + self.pause() + # This will make it PRINT_QUEUE_SIZE lines in front of what # is being sent to the printer, which is another as much as # 16 gcode commands in front of what's actually being printed. @@ -145,14 +180,16 @@ def _print(self, from_line=0): current=current_byte, total=total_size) - if line_index < from_line: - continue - if self.data.paused: log.debug("Pausing USB print") - self.do_instruction("M76") # pause printer's print timer + if self.data.recovering: + self.data.recovery_ready = True + else: + # pause printer's print timer + self.do_instruction("M76") self.wait_for_unpause() + self.data.recovering = False if not self.data.printing: break @@ -163,8 +200,6 @@ def _print(self, from_line=0): if ";LAYER_CHANGE" in line: self.layer_trigger_signal.send() - self.data.line_number = line_index + 1 - gcode = get_gcode(line) if gcode: self.print_gcode(gcode) self.wait_for_queue() @@ -175,26 +210,29 @@ def _print(self, from_line=0): if not self.data.printing: break - self.do_instruction("M77") # stop printer's print timer - log.debug("Print ended") + # Print ended + self._print_end() - if self.pp_exists: - os.remove(self.data.pp_file_path) - self.data.printing = False - self.data.enqueued.clear() + def _print_end(self): + """Handles the end of a file print""" + self.data.enqueued.clear() + self.print_stats.reset_stats() + log.debug("Print ended") - if self.data.stopped_forcefully: - self.serial_queue.flush_print_queue() - self.data.enqueued.clear() # Ensure this gets cleared - # This results in double stop on 3.10 hopefully will get - # changed - # Prevents the print head from stopping in the print - enqueue_instruction(self.serial_queue, "M603", to_front=True) - self.print_stopped_signal.send(self) - else: - self.print_finished_signal.send(self) + if self.data.power_panic: + return + + self.do_instruction("M77") # stop printer's print timer - self.print_stats.reset_stats() + self.data.printing = False + + if self.data.was_stopped: + self.serial_queue.flush_print_queue() + # Prevents the print head from stopping in the print + enqueue_instruction(self.serial_queue, "M603", to_front=True) + self.print_stopped_signal.send(self) + else: + self.print_finished_signal.send(self) def do_instruction(self, message): """Shorthand for enqueueing and waiting for an instruction @@ -209,22 +247,24 @@ def print_gcode(self, gcode): """Sends a gcode to print, keeps a small buffer of gcodes and inlines print stats for files without them (estimated time left and progress)""" - self.data.gcode_number += 1 + with self.lock: + self.data.gcode_number += 1 - divisible = self.data.gcode_number % STATS_EVERY == 0 - if divisible: - time_printing = int(self.print_stats.get_time_printing()) - self.time_printing_signal.send(self, time_printing=time_printing) + divisible = self.data.gcode_number % STATS_EVERY == 0 + if divisible: + time_printing = int(self.print_stats.get_time_printing()) + self.time_printing_signal.send( + self, time_printing=time_printing) - if self.to_print_stats(self.data.gcode_number): - self.send_print_stats() + if self.to_print_stats(self.data.gcode_number): + self.send_print_stats() - log.debug("USB enqueuing gcode: %s", gcode) - instruction = enqueue_instruction(self.serial_queue, - gcode, - to_front=True, - to_checksum=True) - self.data.enqueued.append(instruction) + log.debug("USB enqueuing gcode: %s", gcode) + instruction = enqueue_instruction(self.serial_queue, + gcode, + to_front=True, + to_checksum=True) + self.data.enqueued.append(instruction) def wait_for_queue(self) -> None: """Gets rid of already confirmed messages and waits for any @@ -256,19 +296,6 @@ def react_to_gcode(self, gcode): if gcode.startswith("M601") or gcode.startswith("M25"): self.pause() - def power_panic(self): - """Not used/working""" - # when doing this again don't forget to write print time - if self.data.printing: - self.pause() - self.serial_queue.closed = True - log.warning("POWER PANIC!") - with open(self.data.pp_file_path, "w", - encoding='utf-8') as pp_file: - pp_file.write(f"{self.data.line_number}") - pp_file.flush() - os.fsync(pp_file.fileno()) - def send_print_stats(self): """Sends a gcode to the printer, which tells it the progress percentage and estimated time left, the printer is expected to send @@ -331,6 +358,60 @@ def stop_print(self): print has been stopped and did not finish on its own""" # TODO: wrong, needs to be in line with the rest of commands if self.data.printing: - self.data.stopped_forcefully = True + self.data.was_stopped = True self.data.printing = False self.data.paused = False + + def write_file_stats(self, file_path, message_number, gcode_number): + """Writes the data needed for power panic recovery""" + data = PPData( + file_path=file_path, + message_number=message_number, + gcode_number=gcode_number, + ) + with open(self.data.pp_file_path, "w") as pp_file: + pp_file.write(json.dumps(data.dict())) + + def serial_message_number_changed(self, message_number): + """Updates the pairing of the FW message number to gcode line number + + If all the instructions in the buffer are sent + The message number belongs to the next instruction + that will be sent + + Here's an illustration of the situation + _________________________________________ + |enqueued |gcode_number|message_number| + | | current=25 | current=100 | + |___________|____________|______________| + |next instr.| 26 | 102 | + | I0 | *25* | 101 | + | I1 | 24 | *100* | + | I2 (sent) | 23 | 99 | + | I3 (sent) | 22 | 98 | + |___________|____________|______________| + """ + + with self.lock: + instruction_gcode_number = self.data.gcode_number + 1 + for instruction in self.data.enqueued: + if instruction.is_sent(): + break + instruction_gcode_number -= 1 + self.write_file_stats(self.data.file_path, message_number, + instruction_gcode_number) + + def recover_from_pp(self, message_number): + """Gets the file path and the gcode number to start from + calls the start print with the correct arguments to recover""" + if not self.pp_exists: + log.warning("Cannot recover from power panic, no pp state found") + raise RuntimeError("Cannot recover from power panic, " + "no pp state found") + + with open(self.data.pp_file_path, "r") as pp_file: + pp_data = PPData(**json.load(pp_file)) + + gcode_number = (pp_data.gcode_number + + (message_number - pp_data.message_number)) + self.print(pp_data.file_path, gcode_number) diff --git a/prusa/link/printer_adapter/lcd_printer.py b/prusa/link/printer_adapter/lcd_printer.py index 1374028d..669b79ac 100644 --- a/prusa/link/printer_adapter/lcd_printer.py +++ b/prusa/link/printer_adapter/lcd_printer.py @@ -86,6 +86,7 @@ ERROR_GRACE = 15 +RECOVERY_PRIORITY = 60 PRINT_PRIORITY = 50 WIZARD_PRIORITY = 40 ERROR_PRIORITY = 30 @@ -141,11 +142,12 @@ def __init__(self, self.wait_screen = Screen(resets_idle=False) self.ready_screen = Screen(resets_idle=False) self.idle_screen = Screen(resets_idle=False) + self.recovery_screen = Screen(resets_idle=False) self.carousel = Carousel([ self.print_screen, self.wizard_screen, self.wait_screen, self.error_screen, self.upload_screen, self.ready_screen, - self.idle_screen, + self.idle_screen, self.recovery_screen, ]) self.carousel.set_priority(self.print_screen, PRINT_PRIORITY) @@ -154,6 +156,7 @@ def __init__(self, self.carousel.set_priority(self.upload_screen, UPLOAD_PRIORITY) self.carousel.set_priority(self.ready_screen, READY_PRIORITY) self.carousel.set_priority(self.idle_screen, IDLE_PRIORITY) + self.carousel.set_priority(self.recovery_screen, RECOVERY_PRIORITY) wait_zip = zip(["Please wait"] * 7, ["." * i for i in range(1, 8)]) wait_text = "".join(("".join(i).ljust(19) for i in wait_zip)) @@ -170,6 +173,12 @@ def __init__(self, first_line_extra=0, last_line_extra=0) + self.carousel.set_text(self.recovery_screen, + "Ready to recover", + scroll_delay=5, + first_line_extra=0, + last_line_extra=0) + # Need to implement this in state manager. Only problem is, it's driven # Cannot update itself. For now, this is the workaround self.ignore_errors_to = 0 @@ -226,6 +235,7 @@ def whats_going_on(self): self._check_upload() self._check_ready() self._check_idle() + self._check_recovery() def _check_printing(self): """Should a printing display be activated? And what should it say?""" @@ -424,6 +434,13 @@ def _check_ready(self): else: self.carousel.disable(self.ready_screen) + def _check_recovery(self): + """Should the ready screen be shown?""" + if self.model.file_printer.recovery_ready: + self.carousel.enable(self.recovery_screen) + else: + self.carousel.disable(self.ready_screen) + def _check_idle(self): """Should the idle screen be shown? And what should it say?""" if time() - self.idle_from > SLEEP_SCREEN_TIMEOUT and LAN: diff --git a/prusa/link/printer_adapter/printer_polling.py b/prusa/link/printer_adapter/printer_polling.py index 8beed547..a832a460 100644 --- a/prusa/link/printer_adapter/printer_polling.py +++ b/prusa/link/printer_adapter/printer_polling.py @@ -29,7 +29,7 @@ from ..serial.helpers import enqueue_matchable, wait_for_instruction from ..serial.serial_parser import ThreadedSerialParser from ..serial.serial_queue import SerialQueue -from ..util import get_d3_code, make_fingerprint +from ..util import _parse_little_endian_uint32, get_d3_code, make_fingerprint from .filesystem.sd_card import SDCard from .job import Job from .model import Model @@ -782,9 +782,7 @@ def _eeprom_little_endian_uint32(self, dcode): match = self.do_matchable(dcode, D3_OUTPUT_REGEX, to_front=True) - str_data = match.group("data").replace(" ", "") - data = bytes.fromhex(str_data) - return struct.unpack(" None: TM_CAL_START_REGEX, self.block_serial_queue) self.serial_parser.add_decoupled_handler( TM_CAL_END_REGEX, self.unblock_serial_queue) + self.serial_parser.add_decoupled_handler( + POWER_PANIC_REGEX, self.power_panic_observed) + self.serial_parser.add_decoupled_handler( + PP_RECOVER_REGEX, self.recover_from_pp) self.print_stat_doubler = PrintStatDoubler(self.serial_parser, self.printer_polling) @@ -260,6 +270,8 @@ def __init__(self, cfg: Config, settings: Settings) -> None: self.serial.renewed_signal.connect(self.serial_renewed) self.serial_queue.instruction_confirmed_signal.connect( self.instruction_confirmed) + self.serial_queue.message_number_changed.connect( + self.serial_message_number_changed) self.serial_parser.add_decoupled_handler(PRINTER_BOOT_REGEX, self.printer_reconnected) self.serial_parser.add_decoupled_handler(TM_ERROR_LOG_REGEX, @@ -347,12 +359,10 @@ def __init__(self, cfg: Config, settings: Settings) -> None: self.command_queue.start() self.telemetry_passer.start() self.printer.start() - # Start this last, as it might start printing right away - self.file_printer.start() log.debug("Initialization done") - debug = False + debug = True if debug: Thread(target=self.debug_shell, name="debug_shell", daemon=True).start() @@ -390,6 +400,8 @@ def debug_shell(self) -> None: result = enqueue_matchable( self.serial_queue, "M117 Breaking", re.compile(r"something the printer will not tell us")) + elif command == "pp": + self.serial.power_panic_observed() if result: print(result) # pylint: disable=bare-except @@ -737,8 +749,17 @@ def observed_serial_pause(self) -> None: If the printer says the serial print is paused, but we're not serial printing at all, we'll resolve it by stopping whatever was going on before. + If the serial print is recovering, we tell that to connnect """ - if not self.model.file_printer.printing: + if self.model.file_printer.recovering: + self.state_manager.expect_change( + StateChange(to_states={State.PAUSED: Source.FIRMWARE}, + reason="Waiting for the user to recover the print " + "after a power failure.")) + self.state_manager.paused() + self.state_manager.stop_expecting_change() + if (not self.model.file_printer.printing + and not self.model.file_printer.recovering): self.command_queue.enqueue_command(StopPrint()) def observed_no_print(self) -> None: @@ -796,11 +817,13 @@ def file_printer_finished_printing(self, _) -> None: def serial_failed(self, _) -> None: """Connects serial errors with state manager""" self.state_manager.serial_error() + self.file_printer.stop_print() def serial_renewed(self, _) -> None: """Connects serial recovery with state manager""" self.state_manager.serial_error_resolved() self.printer_reconnected() + log.warning("Serial renewed") def set_sn(self, _, serial_number: str) -> None: """Set serial number and fingerprint""" @@ -852,6 +875,11 @@ def instruction_confirmed(self, _) -> None: """ self.state_manager.instruction_confirmed() + def serial_message_number_changed(self, message_number): + """Connects serial message number change to file printer + for power panic to work""" + self.file_printer.serial_message_number_changed(message_number) + def block_serial_queue(self, *_, **__) -> None: """Blocks the serial queue""" self.serial_queue.block_sending() @@ -860,6 +888,21 @@ def unblock_serial_queue(self, *_, **__) -> None: """Unblocks the serial queue""" self.serial_queue.unblock_sending() + def power_panic_observed(self, *_, **__): + """Routes a power panic message to components""" + self.file_printer.power_panic() + self.state_manager.power_panic_observed() + self.serial.power_panic_observed() + self.state_manager.paused() + # This is normally a bad idea in a serial handler + # But as we are holding the serial disconnected anyways, it's OK + sleep(10) + self.serial.power_panic_unblock() + + def recover_from_pp(self, *_, **__) -> None: + """Recover from power panic""" + self.command_queue.enqueue_command(PPRecovery()) + def printer_reconnected(self, *_, **__) -> None: """ Connects the printer reconnect (reset) to many other components. diff --git a/prusa/link/printer_adapter/state_manager.py b/prusa/link/printer_adapter/state_manager.py index d31ab4fd..c3e4e288 100644 --- a/prusa/link/printer_adapter/state_manager.py +++ b/prusa/link/printer_adapter/state_manager.py @@ -22,8 +22,7 @@ CANCEL_REGEX, ERROR_REASON_REGEX, ERROR_REGEX, FAN_ERROR_REGEX, FAN_REGEX, PAUSED_REGEX, - RESUMED_REGEX, TM_ERROR_CLEARED, - POWER_PANIC_REGEX) + RESUMED_REGEX, TM_ERROR_CLEARED) log = logging.getLogger(__name__) @@ -198,7 +197,6 @@ def __init__(self, serial_parser: ThreadedSerialParser, model: Model): ATTENTION_REASON_REGEX: self.attention_reason_handler, FAN_ERROR_REGEX: self.fan_error, TM_ERROR_CLEARED: self.clear_tm_error, - POWER_PANIC_REGEX: self.power_panic_observed, } for regex, handler in regex_handlers.items(): @@ -528,9 +526,8 @@ def clear_tm_error(self, _, match: re.Match): assert match is not None self.tm_ignore_pause = False - def power_panic_observed(self, _, match: re.Match): + def power_panic_observed(self): """Set the power panic flag""" - assert match is not None self.in_power_panic = True def reset_power_panic(self): diff --git a/prusa/link/printer_adapter/structures/model_classes.py b/prusa/link/printer_adapter/structures/model_classes.py index fd7c9dd4..ed0f252f 100644 --- a/prusa/link/printer_adapter/structures/model_classes.py +++ b/prusa/link/printer_adapter/structures/model_classes.py @@ -139,3 +139,12 @@ class EEPROMParams(Enum): ACTIVE_SHEET = 0x0DA1, 1 TOTAL_FILAMENT = 0x0FF1, 4 TOTAL_PRINT_TIME = 0x0FED, 4 + EEPROM_FILE_POSITION = 0x0F91, 4 + + +class PPData(BaseModel): + """Not things like length or diameter, + just path and the command number -> gcode command number""" + file_path: str + message_number: int # N number on the printer + gcode_number: int # From file printer diff --git a/prusa/link/printer_adapter/structures/module_data_classes.py b/prusa/link/printer_adapter/structures/module_data_classes.py index cb3d847d..35059465 100644 --- a/prusa/link/printer_adapter/structures/module_data_classes.py +++ b/prusa/link/printer_adapter/structures/module_data_classes.py @@ -47,9 +47,12 @@ class FilePrinterData(BaseModel): file_path: str pp_file_path: str printing: bool + recovering: bool paused: bool - stopped_forcefully: bool + was_stopped: bool line_number: int + power_panic: bool + recovery_ready: bool # In reality Deque[Instruction] but that cannot be validated by pydantic enqueued: Deque[Any] diff --git a/prusa/link/printer_adapter/structures/regular_expressions.py b/prusa/link/printer_adapter/structures/regular_expressions.py index c5b48ba0..538bf245 100644 --- a/prusa/link/printer_adapter/structures/regular_expressions.py +++ b/prusa/link/printer_adapter/structures/regular_expressions.py @@ -155,3 +155,4 @@ RESET_ACTIVATED_REGEX = re.compile(r"^Reset mode activated$") RESET_DEACTIVATED_REGEX = re.compile(r"^Reset mode deactivated$") +PP_RECOVER_REGEX = re.compile(r"^// action:uvlo_recovery_ready$") diff --git a/prusa/link/serial/serial_adapter.py b/prusa/link/serial/serial_adapter.py index 8a5fdca9..ae596b75 100644 --- a/prusa/link/serial/serial_adapter.py +++ b/prusa/link/serial/serial_adapter.py @@ -5,7 +5,7 @@ import re from importlib import util from pathlib import Path -from threading import RLock +from threading import Event, RLock from time import sleep, time from typing import List, Optional @@ -17,6 +17,7 @@ from ..const import ( PRINTER_BOOT_WAIT, PRINTER_TYPES, + QUIT_INTERVAL, RESET_PIN, SERIAL_REOPEN_TIMEOUT, ) @@ -102,6 +103,8 @@ def __init__(self, self.renewed_signal = Signal() self.running = True + self.work_around_power_panic = Event() + self.work_around_power_panic.set() self.read_thread = Thread(target=self._read_continually, name="serial_read_thread", @@ -256,8 +259,15 @@ def _renew_serial_connection(self, starting: bool = False): Informs the rest of the app about failed serial connection, After which it keeps trying to re-open the serial port - If it succeeds, generates a signal to remove the rest of the app + If it succeeds, generates a signal to inform the rest of the app """ + if not self.work_around_power_panic.is_set(): + self.failed_signal.send(self) + SERIAL.state = CondState.NOK + + while self.running: + if self.work_around_power_panic.wait(QUIT_INTERVAL): + break if self.is_open(self.serial): raise RuntimeError("Don't reconnect what is not disconnected") @@ -267,6 +277,7 @@ def _renew_serial_connection(self, starting: bool = False): starting = False else: self.failed_signal.send(self) + SERIAL.state = CondState.NOK if not self._reopen(): SERIAL.state = CondState.NOK @@ -291,6 +302,9 @@ def _read_continually(self): raw_line = "[No data] - This is a fallback value, " \ "so stuff doesn't break" try: + if not self.work_around_power_panic.is_set(): + raise SerialException( + "Need to re-connect after power panic") raw_line = self.serial.readline() line = decode_line(raw_line) except (SerialException, OSError): @@ -401,3 +415,11 @@ def stop(self): def wait_stopped(self): """Waits for the serial to be stopped""" self.read_thread.join() + + def power_panic_observed(self): + """Called when a power panic is observed""" + self.work_around_power_panic.clear() + + def power_panic_unblock(self): + """Re-sets the power panic flag that holds the serial disconnected""" + self.work_around_power_panic.set() diff --git a/prusa/link/serial/serial_queue.py b/prusa/link/serial/serial_queue.py index 270986e8..01a03b9a 100644 --- a/prusa/link/serial/serial_queue.py +++ b/prusa/link/serial/serial_queue.py @@ -68,6 +68,7 @@ def __init__(self, # printer, let's signal this to other modules self.serial_queue_failed = Signal() self.instruction_confirmed_signal = Signal() + self.message_number_changed = Signal() # A queue of instructions for the printer self.queue: Deque[Instruction] = deque() @@ -329,8 +330,18 @@ def _send(self): self._hookup_output_capture() self.current_instruction.sent() + + # Send the message number only after the instruction is sent + if m110_match: + self.message_number_changed.send(self.message_number) + self.serial_adapter.write(self.current_instruction.data) + def set_message_number(self, number): + """Sets the message number to the given value""" + with self.write_lock: + self.message_number = number + def _enqueue(self, instruction: Instruction, to_front=False): """Internal method for enqueuing when already locked""" if to_front: diff --git a/prusa/link/util.py b/prusa/link/util.py index 0c4168bc..35e233f3 100644 --- a/prusa/link/util.py +++ b/prusa/link/util.py @@ -5,6 +5,7 @@ import os import pwd import socket +import struct import typing from hashlib import sha256 from pathlib import Path @@ -287,3 +288,10 @@ def walk_dict(data: dict, key_path=None): yield from walk_dict(value, key_path + [key]) else: yield key_path + [key], value + + +def _parse_little_endian_uint32(match): + """Decodes the D-Code specified little-endian uint32_t eeprom variable""" + str_data = match.group("data").replace(" ", "") + data = bytes.fromhex(str_data) + return struct.unpack("