From 5f33bbae82d976627b4eaa1780da75d779bbcf08 Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Fri, 29 Nov 2024 18:25:49 +0100 Subject: [PATCH 1/4] Remove two-second delay before "finish" sound There were two sources for the delay: one deliberate delay of 1 second to "finish printing to knit progress window", which does not seem required anymore; and another caused by waiting for the serial port to be closed before emitting the finished sound: this was fixed by coupling the audio emission to printing the notification in the progress bar, rather than the actual end of the complete operation. --- src/main/python/main/ayab/ayab.py | 9 ++------- src/main/python/main/ayab/engine/engine.py | 7 +------ src/main/python/main/ayab/engine/output.py | 1 + src/main/python/main/ayab/signal_receiver.py | 2 +- src/main/python/main/ayab/signal_sender.py | 14 +++++--------- 5 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/main/python/main/ayab/ayab.py b/src/main/python/main/ayab/ayab.py index 2bde0477..9fd424ee 100644 --- a/src/main/python/main/ayab/ayab.py +++ b/src/main/python/main/ayab/ayab.py @@ -106,9 +106,7 @@ def __activate_ui(self) -> None: self.scene.ayabimage.select_file ) self.ui.cancel_button.clicked.connect(self.engine.cancel) - self.hw_test.finished.connect( - lambda: self.finish_operation(Operation.TEST, False) - ) + self.hw_test.finished.connect(lambda: self.finish_operation(Operation.TEST)) def __activate_menu(self) -> None: self.menu.ui.action_open_image_file.triggered.connect( @@ -158,7 +156,7 @@ def start_operation(self) -> None: self.ui.open_image_file_button.setEnabled(False) self.menu.setEnabled(False) - def finish_operation(self, operation: Operation, beep: bool) -> None: + def finish_operation(self, operation: Operation) -> None: """(Re-)enable UI elements after operation finishes.""" if operation == Operation.KNIT: self.knit_thread.wait() @@ -169,9 +167,6 @@ def finish_operation(self, operation: Operation, beep: bool) -> None: self.ui.open_image_file_button.setEnabled(True) self.menu.setEnabled(True) - if operation == Operation.KNIT and beep: - self.audio.play("finish") - def set_image_dimensions(self) -> None: """Set dimensions of image.""" width, height = self.scene.ayabimage.image.size diff --git a/src/main/python/main/ayab/engine/engine.py b/src/main/python/main/ayab/engine/engine.py index 43aee652..618945c5 100644 --- a/src/main/python/main/ayab/engine/engine.py +++ b/src/main/python/main/ayab/engine/engine.py @@ -20,7 +20,6 @@ from __future__ import annotations import logging -from time import sleep from PIL import Image from PySide6.QtCore import QCoreApplication, Signal @@ -190,16 +189,12 @@ def run(self, operation: Operation) -> None: else: # operation == Operation.TEST: self.__logger.info("Finished knitting.") - # small delay to finish printing to knit progress window - # before "finish.wav" sound plays - sleep(1) else: # TODO: provide translations for these messages self.__logger.info("Finished testing.") # send signal to finish operation - # "finish.wav" sound only plays if knitting was not canceled - self.emit_operation_finisher(operation, not self.__canceled) + self.emit_operation_finisher(operation) def __handle_status(self) -> None: if self.status.active: diff --git a/src/main/python/main/ayab/engine/output.py b/src/main/python/main/ayab/engine/output.py index 437c9596..5c6e153d 100644 --- a/src/main/python/main/ayab/engine/output.py +++ b/src/main/python/main/ayab/engine/output.py @@ -100,6 +100,7 @@ def _next_line(self) -> None: self.emit_audio_player("nextline") def _knitting_finished(self) -> None: + self.emit_audio_player("finish") self.emit_notification( "Image transmission finished. Please knit until you " + "hear the double beep sound." diff --git a/src/main/python/main/ayab/signal_receiver.py b/src/main/python/main/ayab/signal_receiver.py index d3932a79..d256a07f 100644 --- a/src/main/python/main/ayab/signal_receiver.py +++ b/src/main/python/main/ayab/signal_receiver.py @@ -58,7 +58,7 @@ class SignalReceiver(QObject): new_image_flag = Signal() bad_config_flag = Signal() knitting_starter = Signal() - operation_finisher = Signal(Operation, bool) + operation_finisher = Signal(Operation) hw_test_starter = Signal(Control) hw_test_writer = Signal(str) diff --git a/src/main/python/main/ayab/signal_sender.py b/src/main/python/main/ayab/signal_sender.py index 4dbc2416..6efcb854 100644 --- a/src/main/python/main/ayab/signal_sender.py +++ b/src/main/python/main/ayab/signal_sender.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: from .signal_receiver import SignalReceiver from .utils import MessageTypes - from .engine.status import ColorSymbolType, Status + from .engine.status import Status from .engine.engine_fsm import Operation from .engine.options import Alignment from .engine.control import Control @@ -49,12 +49,8 @@ def __init__( def emit_start_row_updater(self, start_row: int) -> None: self.__signal_receiver.start_row_updater.emit(start_row) - def emit_progress_bar_updater( - self, status: Status - ) -> None: - self.__signal_receiver.progress_bar_updater.emit( - status - ) + def emit_progress_bar_updater(self, status: Status) -> None: + self.__signal_receiver.progress_bar_updater.emit(status) def emit_knit_progress_updater( self, status: Status, row_multiplier: int, midline: int, auto_mirror: bool @@ -101,8 +97,8 @@ def emit_bad_config_flag(self) -> None: def emit_knitting_starter(self) -> None: self.__signal_receiver.knitting_starter.emit() - def emit_operation_finisher(self, operation: Operation, beep: bool) -> None: - self.__signal_receiver.operation_finisher.emit(operation, beep) + def emit_operation_finisher(self, operation: Operation) -> None: + self.__signal_receiver.operation_finisher.emit(operation) def emit_hw_test_starter(self, control: Control) -> None: self.__signal_receiver.hw_test_starter.emit(control) From b234a817d0df1b9456489740801fdcc04154cdd6 Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Tue, 3 Dec 2024 07:02:07 +0100 Subject: [PATCH 2/4] Send a blank final line to the firmware --- src/main/python/main/ayab/engine/control.py | 20 +++++++++-- .../python/main/ayab/engine/engine_fsm.py | 36 ++++++++++++------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/main/python/main/ayab/engine/control.py b/src/main/python/main/ayab/engine/control.py index 0f290f1a..c474adc4 100644 --- a/src/main/python/main/ayab/engine/control.py +++ b/src/main/python/main/ayab/engine/control.py @@ -209,9 +209,12 @@ def cnf_line_API6(self, line_number: int) -> bool: color, row_index, blank_line, last_line = self.mode_func(self, line_number) bits = self.select_needles_API6(color, row_index, blank_line) - # send line to machine - flag = last_line and not self.inf_repeat - self.com.cnf_line_API6(requested_line, color, flag, bits.tobytes()) + # Send line to machine + # Note that we never set the "final line" flag here, because + # we will send an extra blank line afterwards to make sure we + # can track the final line being knitted. + flags = 0 + self.com.cnf_line_API6(requested_line, color, flags, bits.tobytes()) # screen output # TODO: tidy up this code @@ -242,6 +245,17 @@ def cnf_line_API6(self, line_number: int) -> bool: else: return True # pattern finished + def cnf_final_line_API6(self, requested_line: int) -> None: + self.logger.debug("sending blank line as final line=%d", requested_line) + + # prepare a blank line as the final line + bits = bitarray(self.machine.width, endian="little") + + # send line to machine + color = 0 # doesn't matter + flags = 1 # this is the last line + self.com.cnf_line_API6(requested_line, color, flags, bits.tobytes()) + def __update_status(self, line_number: int, color: int, bits: bitarray) -> None: self.status.total_rows = self.pat_height self.status.current_row = self.pat_row + 1 diff --git a/src/main/python/main/ayab/engine/engine_fsm.py b/src/main/python/main/ayab/engine/engine_fsm.py index 24a46b91..be81c76d 100644 --- a/src/main/python/main/ayab/engine/engine_fsm.py +++ b/src/main/python/main/ayab/engine/engine_fsm.py @@ -52,6 +52,7 @@ class State(Enum): CONFIRM_TEST = auto() RUN_TEST = auto() DISCONNECT = auto() + FINISHING = auto() FINISHED = auto() @@ -197,18 +198,8 @@ def _API6_run_knit(control: Control, operation: Operation) -> Output: if token == Token.reqLine: pattern_finished = control.cnf_line_API6(param) if pattern_finished: - # When closing the serial port, the final bytes written - # may be dropped by the driver - # (see https://github.com/serialport/serialport-rs/issues/117). - # This may cause the final `cnfLine` response to get lost and the - # firmware to get stuck knitting the previous row - # (see https://github.com/AllYarnsAreBeautiful/ayab-desktop/issues/662). - # To avoid this, before closing the port, we send a `reqInfo` message - # to the firmware and wait for the response. - control.com.req_info() - control.state = State.DISCONNECT - control.logger.debug("State DISCONNECT") - return Output.DISCONNECTING_FROM_MACHINE + control.state = State.FINISHING + return Output.NEXT_LINE else: return Output.NEXT_LINE # else @@ -248,6 +239,27 @@ def _API6_run_test(control: Control, operation: Operation) -> Output: control.check_serial_API6() return Output.NONE + @staticmethod + def _API6_finishing(control: Control, operation: Operation) -> Output: + token, param = control.check_serial_API6() + if token == Token.reqLine: + control.cnf_final_line_API6(param) + + # When closing the serial port, the final bytes written + # may be dropped by the driver + # (see https://github.com/serialport/serialport-rs/issues/117). + # This may cause the final `cnfLine` response to get lost and the + # firmware to get stuck knitting the previous row + # (see https://github.com/AllYarnsAreBeautiful/ayab-desktop/issues/662). + # To avoid this, before closing the port, we send a `reqInfo` message + # to the firmware and wait for the response. + control.com.req_info() + control.state = State.DISCONNECT + control.logger.debug("State DISCONNECT") + return Output.DISCONNECTING_FROM_MACHINE + # else + return Output.NONE + @staticmethod def _API6_disconnect(control: Control, operation: Operation) -> Output: token, _ = control.check_serial_API6() From 3286f99d28cf62527af178c3df305086168399bd Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Tue, 3 Dec 2024 23:21:23 +0100 Subject: [PATCH 3/4] Fix end-of-line sounds in simulation In order to let the UI play the end-of-line sounds, we need the communications mock to not emit reqLine messages back-to-back. --- .../main/ayab/engine/communication_mock.py | 53 +++++++++++-------- .../ayab/tests/test_communication_mock.py | 3 ++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/main/python/main/ayab/engine/communication_mock.py b/src/main/python/main/ayab/engine/communication_mock.py index 837f132e..f90415a5 100644 --- a/src/main/python/main/ayab/engine/communication_mock.py +++ b/src/main/python/main/ayab/engine/communication_mock.py @@ -22,7 +22,6 @@ import logging from time import sleep -from collections import deque from PySide6.QtWidgets import QMessageBox @@ -47,8 +46,9 @@ def __del__(self) -> None: def reset(self): self.__is_open = False self.__is_started = False - self.rx_msg_list = deque([], maxlen=100) + self.rx_msg_list = list() self.__line_count = 0 + self.__started_row = False def is_open(self) -> bool: """Return status of the interface.""" @@ -109,29 +109,36 @@ def cnf_line_API6(self, line_number, color, flags, line_data) -> bool: """Send a row of stitch data.""" return True - def update_API6(self) -> tuple[bytes, Token, int]: + def update_API6(self) -> tuple[bytes | None, Token, int]: """Read and parse data packet.""" if self.__is_open and self.__is_started: - reqLine = bytes([Token.reqLine.value, self.__line_count]) - self.__line_count += 1 - self.__line_count %= 256 - self.rx_msg_list.append(reqLine) - if self.__delay: - sleep(1) # wait for knitting progress dialog to update - # step through output line by line - if self.__step: - # pop up box waits for user input before moving on to next line - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Information) - msg.setText("Line number = " + str(self.__line_count)) - msg.setStandardButtons( - QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel - ) - ret = None - ret = msg.exec_() - while ret is None: - pass + # Alternate between reqLine and no message + # (so that the UI makes the end-of-line sound for each row) + if self.__started_row: + self.__started_row = False + reqLine = bytes([Token.reqLine.value, self.__line_count]) + self.__line_count += 1 + self.__line_count %= 256 + self.rx_msg_list.append(reqLine) + if self.__delay: + sleep(1) # wait for knitting progress dialog to update + # step through output line by line + if self.__step: + # pop up box waits for user input before moving on to next line + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Information) + msg.setText("Line number = " + str(self.__line_count)) + msg.setStandardButtons( + QMessageBox.StandardButton.Ok + | QMessageBox.StandardButton.Cancel + ) + ret = None + ret = msg.exec_() + while ret is None: + pass + else: + self.__started_row = True if len(self.rx_msg_list) > 0: - return self.parse_API6(self.rx_msg_list.popleft()) # FIFO + return self.parse_API6(self.rx_msg_list.pop(0)) # FIFO # else return self.parse_API6(None) diff --git a/src/main/python/main/ayab/tests/test_communication_mock.py b/src/main/python/main/ayab/tests/test_communication_mock.py index 8bee5a86..452efc6b 100644 --- a/src/main/python/main/ayab/tests/test_communication_mock.py +++ b/src/main/python/main/ayab/tests/test_communication_mock.py @@ -107,6 +107,9 @@ def test_req_line_API6(self): ) self.comm_dummy.update_API6() # cnfStart + # Alternates between requesting a line and no output for i in range(0, 256): bytes_read = self.comm_dummy.update_API6() assert bytes_read == (bytearray([Token.reqLine.value, i]), Token.reqLine, i) + bytes_read = self.comm_dummy.update_API6() + assert bytes_read == (None, Token.none, 0) From 5f9e751fe248626542e96c6a002b435eee4b284e Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Tue, 3 Dec 2024 23:41:59 +0100 Subject: [PATCH 4/4] Remove "step" code from CommunicationMock This did not really work, due to running on a non-UI thread. --- .../main/ayab/engine/communication_mock.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/python/main/ayab/engine/communication_mock.py b/src/main/python/main/ayab/engine/communication_mock.py index f90415a5..91c98937 100644 --- a/src/main/python/main/ayab/engine/communication_mock.py +++ b/src/main/python/main/ayab/engine/communication_mock.py @@ -23,20 +23,17 @@ import logging from time import sleep -from PySide6.QtWidgets import QMessageBox - from .communication import Communication, Token class CommunicationMock(Communication): """Class Handling the mock communication protocol.""" - def __init__(self, delay=True, step=False) -> None: + def __init__(self, delay=True) -> None: """Initialize communication.""" logging.basicConfig(level=logging.DEBUG) self.logger = logging.getLogger(type(self).__name__) self.__delay = delay - self.__step = step self.reset() def __del__(self) -> None: @@ -122,20 +119,6 @@ def update_API6(self) -> tuple[bytes | None, Token, int]: self.rx_msg_list.append(reqLine) if self.__delay: sleep(1) # wait for knitting progress dialog to update - # step through output line by line - if self.__step: - # pop up box waits for user input before moving on to next line - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Information) - msg.setText("Line number = " + str(self.__line_count)) - msg.setStandardButtons( - QMessageBox.StandardButton.Ok - | QMessageBox.StandardButton.Cancel - ) - ret = None - ret = msg.exec_() - while ret is None: - pass else: self.__started_row = True if len(self.rx_msg_list) > 0: