Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronise end-of-line sounds between app and firmware #731

Merged
merged 4 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions src/main/python/main/ayab/ayab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
44 changes: 17 additions & 27 deletions src/main/python/main/ayab/engine/communication_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,18 @@

import logging
from time import sleep
from collections import deque

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:
Expand All @@ -47,8 +43,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."""
Expand Down Expand Up @@ -109,29 +106,22 @@ 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
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)
20 changes: 17 additions & 3 deletions src/main/python/main/ayab/engine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions src/main/python/main/ayab/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from __future__ import annotations
import logging
from time import sleep
from PIL import Image

from PySide6.QtCore import QCoreApplication, Signal
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 24 additions & 12 deletions src/main/python/main/ayab/engine/engine_fsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class State(Enum):
CONFIRM_TEST = auto()
RUN_TEST = auto()
DISCONNECT = auto()
FINISHING = auto()
FINISHED = auto()


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/main/python/main/ayab/engine/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/main/python/main/ayab/signal_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 5 additions & 9 deletions src/main/python/main/ayab/signal_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/main/python/main/ayab/tests/test_communication_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)