From d4b2fec1dbe6e987a94857f844e4825fefb1559f Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 23 May 2026 17:10:46 -0400 Subject: [PATCH] speculative fixes for hardware speech. --- src/fenrirscreenreader/fenrirVersion.py | 2 +- .../speechDriver/hardwareSerialDriver.py | 16 +++++-- tests/unit/test_hardware_speech_drivers.py | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index fbee5d53..3bdf3843 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -4,5 +4,5 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. -version = "2026.05.22" +version = "2026.05.23" code_name = "testing" diff --git a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py index e006b865..d733d270 100644 --- a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py +++ b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py @@ -51,6 +51,8 @@ class hardware_serial_driver(speech_driver): ) self._open_serial_port() self._is_initialized = self.serial_port is not None + if not self._is_initialized: + raise RuntimeError("hardware speech device is not available") if self._is_initialized: self._stop_worker = False self.worker_thread = threading.Thread( @@ -124,6 +126,7 @@ class hardware_serial_driver(speech_driver): self._debug( "Hardware speech device not found", debug.DebugLevel.ERROR, + on_any_level=True, ) return try: @@ -140,10 +143,16 @@ class hardware_serial_driver(speech_driver): termios.tcsetattr(port, termios.TCSANOW, attrs) self.serial_port = port self.device = device + self._debug( + f"Hardware speech device opened: {device}", + debug.DebugLevel.INFO, + on_any_level=True, + ) except OSError as error: self._debug( f"Hardware speech device open failed: {device}: {error}", debug.DebugLevel.ERROR, + on_any_level=True, ) self.serial_port = None @@ -173,12 +182,13 @@ class hardware_serial_driver(speech_driver): self._debug( f"Hardware speech write failed: {error}", debug.DebugLevel.ERROR, + on_any_level=True, ) def _resolve_device(self, device): if device and device != "auto": return device - for pattern in ("/dev/ttyACM*", "/dev/ttyUSB*"): + for pattern in ("/dev/ttyACM*", "/dev/ttyUSB*", "/dev/ttyS*"): matches = sorted(glob.glob(pattern)) if matches: return matches[0] @@ -205,10 +215,10 @@ class hardware_serial_driver(speech_driver): value = max(0.0, min(1.0, value)) return int(round(minimum + value * (maximum - minimum))) - def _debug(self, message, level): + def _debug(self, message, level, on_any_level=False): try: self.env["runtime"]["DebugManager"].write_debug_out( - message, level + message, level, on_any_level=on_any_level ) except Exception: pass diff --git a/tests/unit/test_hardware_speech_drivers.py b/tests/unit/test_hardware_speech_drivers.py index 8f5fce86..e4a5a8b5 100644 --- a/tests/unit/test_hardware_speech_drivers.py +++ b/tests/unit/test_hardware_speech_drivers.py @@ -1,6 +1,7 @@ import os import select import time +from unittest.mock import ANY from unittest.mock import Mock import pytest @@ -103,6 +104,48 @@ def test_litetalk_driver_writes_settings_and_cancel(serial_pair): speech_driver.shutdown() +def test_auto_device_detection_includes_classic_serial( + monkeypatch, serial_pair +): + master_fd, slave_name = serial_pair + + def fake_glob(pattern): + if pattern == "/dev/ttyS*": + return [slave_name] + return [] + + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", + fake_glob, + ) + speech_driver = litetalkDriver.driver() + speech_driver.initialize(build_environment("auto")) + try: + assert speech_driver.device == slave_name + speech_driver.speak("Serial") + assert read_available(master_fd, 7) == b"Serial\r" + finally: + speech_driver.shutdown() + + +def test_auto_device_detection_fails_when_no_serial_device(monkeypatch): + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", + lambda pattern: [], + ) + speech_driver = litetalkDriver.driver() + + with pytest.raises(RuntimeError, match="hardware speech device"): + speech_driver.initialize(build_environment("auto")) + + debug_manager = speech_driver.env["runtime"]["DebugManager"] + debug_manager.write_debug_out.assert_called_with( + "Hardware speech device not found", + ANY, + on_any_level=True, + ) + + @pytest.mark.parametrize("driver_class", [doubletalkDriver, tripletalkDriver]) def test_litetalk_compatible_alias_drivers(driver_class, serial_pair): speech_driver, master_fd = initialized_driver(driver_class, serial_pair)