diff --git a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py index 12c66d90..8eb05b89 100644 --- a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py +++ b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py @@ -322,31 +322,42 @@ class hardware_serial_driver(speech_driver): ) return [device] devices = [] + seen_devices = set() for pattern in ( "/dev/serial/by-id/*", "/dev/ttyACM*", "/dev/ttyUSB*", + "/dev/ttyS0", + "/dev/ttyS1", ): matches = sorted(glob.glob(pattern)) + unique_matches = [] + for match in matches: + real_device = os.path.realpath(match) + if real_device in seen_devices: + continue + seen_devices.add(real_device) + unique_matches.append(match) self._debug( f"Hardware speech auto scan {pattern}: {matches}", debug.DebugLevel.INFO, on_any_level=True, ) - if matches: - if len(matches) > 1: + if unique_matches: + if len(unique_matches) > 1: self._debug( "Hardware speech auto found multiple devices for " - f"{pattern}: {matches}; probing in order", + f"{pattern}: {unique_matches}; probing in order", debug.DebugLevel.WARNING, on_any_level=True, ) self._debug( - f"Hardware speech auto candidate devices: {matches}", + "Hardware speech auto candidate devices: " + f"{unique_matches}", debug.DebugLevel.INFO, on_any_level=True, ) - devices.extend(matches) + devices.extend(unique_matches) return devices def _termios_baud_rate(self, baud_rate): diff --git a/src/fenrirscreenreader/speechDriver/litetalkDriver.py b/src/fenrirscreenreader/speechDriver/litetalkDriver.py index 75397781..cd6def28 100644 --- a/src/fenrirscreenreader/speechDriver/litetalkDriver.py +++ b/src/fenrirscreenreader/speechDriver/litetalkDriver.py @@ -12,6 +12,7 @@ from fenrirscreenreader.speechDriver.hardwareSerialDriver import ( class driver(hardware_serial_driver): cancel_command = b"\x18" hardware_probe_command = b"\x01I" + hardware_probe_timeout = 3.5 def _speak_bytes(self, text): return self._clean_text(text).encode("ascii", errors="replace") + b"\r" diff --git a/tests/unit/test_hardware_speech_drivers.py b/tests/unit/test_hardware_speech_drivers.py index 0e7c36c5..fdfd9795 100644 --- a/tests/unit/test_hardware_speech_drivers.py +++ b/tests/unit/test_hardware_speech_drivers.py @@ -166,6 +166,9 @@ def test_auto_device_detection_prefers_probe_response(monkeypatch): def fake_select(readable, writable, exceptional, timeout): port = readable[0] + if timeout == 0: + return [], writable, exceptional + assert timeout == 3.5 if port == 101 and writes.get(port) == b"\x01I": return readable, writable, exceptional return [], writable, exceptional @@ -273,6 +276,70 @@ def test_auto_device_detection_fails_without_probe_response( assert speech_driver.device == "auto" +def test_auto_device_detection_deduplicates_serial_by_id(monkeypatch): + opened_devices = [] + + def fake_glob(pattern): + if pattern == "/dev/serial/by-id/*": + return ["/dev/serial/by-id/modem0"] + if pattern == "/dev/ttyUSB*": + return ["/dev/ttyUSB0"] + return [] + + def fake_realpath(path): + if path in ("/dev/serial/by-id/modem0", "/dev/ttyUSB0"): + return "/dev/ttyUSB0" + return path + + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", + fake_glob, + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.path.realpath", + fake_realpath, + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.open", + lambda device, flags: opened_devices.append(device) + or len(opened_devices), + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.close", + lambda port: None, + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.write", + lambda port, data: len(data), + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.select.select", + lambda readable, writable, exceptional, timeout: ( + [], + writable, + exceptional, + ), + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.termios.tcgetattr", + lambda port: [0, 0, 0, 0, 0, 0, [0] * 32], + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.termios.tcsetattr", + lambda port, when, attrs: None, + ) + monkeypatch.setattr( + "fenrirscreenreader.speechDriver.hardwareSerialDriver.tty.setraw", + lambda port: None, + ) + + speech_driver = litetalkDriver.driver() + with pytest.raises(RuntimeError, match="hardware speech device"): + speech_driver.initialize(build_environment("auto")) + + assert opened_devices == ["/dev/serial/by-id/modem0"] + + def test_auto_device_detection_skips_termios_failures(monkeypatch): opened_ports = [] closed_ports = []