diff --git a/README.md b/README.md index c89ca157..dbc32e25 100644 --- a/README.md +++ b/README.md @@ -467,8 +467,7 @@ setting [parameters] USB hardware synths are supported only when Linux exposes them as a serial tty such as `/dev/ttyACM0` or `/dev/ttyUSB0`. A USB-only TripleTalk with no tty device would require a separate USB protocol driver. Use an explicit -`speech#hardware_device` path for stable hardware speech; `auto` is limited to -devices that respond to a driver-specific probe. +`speech#hardware_device` path for hardware speech. - `speech#auto_read_incoming=True/False` - Auto-read new text *Sound Settings:* diff --git a/config/settings/settings.conf b/config/settings/settings.conf index 8f96cbe4..ea0998ec 100644 --- a/config/settings/settings.conf +++ b/config/settings/settings.conf @@ -79,8 +79,7 @@ volume=1.0 # Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver. # USB serial devices are supported if Linux exposes them as /dev/ttyACM* # or /dev/ttyUSB*. USB-only synths with no tty device need a separate driver. -# Set an explicit device for stable hardware speech. auto only uses devices -# that respond to a driver-specific probe and may fail on silent synths. +# Set an explicit device for hardware speech. # Examples: # hardware_device=/dev/ttyACM0 # RPITalk USB gadget mode # hardware_device=/dev/ttyUSB0 # USB serial adapter diff --git a/docs/fenrir.adoc b/docs/fenrir.adoc index 920d6ad9..1d14ded4 100644 --- a/docs/fenrir.adoc +++ b/docs/fenrir.adoc @@ -1684,15 +1684,12 @@ the pico module: language=de-DE .... -Hardware speech drivers use a serial device. Set an explicit path for stable -systems. `+auto+` is limited to devices that respond to a driver-specific -probe and may fail on silent synths. +Hardware speech drivers use a serial device. Set an explicit path. .... hardware_device=/dev/ttyACM0 hardware_device=/dev/ttyUSB0 hardware_device=/dev/ttyS0 -hardware_device=auto .... Hardware speech drivers use 9600 baud by default. diff --git a/docs/user.md b/docs/user.md index 6df46dcf..9cff2c88 100644 --- a/docs/user.md +++ b/docs/user.md @@ -344,8 +344,7 @@ Fenrir automatically detects and provides audio feedback for progress indicators For hardware speech, set `speech#hardware_device` to an explicit serial path. RPITalk gadget mode usually appears as `/dev/ttyACM0`; USB serial adapters usually appear as `/dev/ttyUSB0`; built-in serial ports may be `/dev/ttyS0`. -The default baud rate is `9600`. `auto` is limited to devices that respond to a -driver-specific probe and may fail on silent synths. `doubletalkDriver` targets +The default baud rate is `9600`. `doubletalkDriver` targets DoubleTalk LT-style serial devices, not the internal DoubleTalk PC ISA card. USB TripleTalk devices work only if Linux exposes them as a serial tty such as `/dev/ttyACM0` or `/dev/ttyUSB0`; USB-only models with no tty device need a diff --git a/docs/user.txt b/docs/user.txt index 41d73e83..b201105d 100644 --- a/docs/user.txt +++ b/docs/user.txt @@ -927,11 +927,10 @@ Select the language you want Fenrir to use. language=english-us Values: Text, see your TTS synths documentation what is available. -Hardware speech drivers use a serial device. Set an explicit path for stable systems. ''auto'' is limited to devices that respond to a driver-specific probe and may fail on silent synths. +Hardware speech drivers use a serial device. Set an explicit path. hardware_device=/dev/ttyACM0 hardware_device=/dev/ttyUSB0 hardware_device=/dev/ttyS0 - hardware_device=auto Hardware speech drivers use 9600 baud by default. hardware_baud_rate=9600 diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py index 80e56bf4..281c3463 100644 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py @@ -127,7 +127,7 @@ class config_command: self.config.set("speech", "rate", "0.75") self.config.set("speech", "pitch", "0.5") self.config.set("speech", "volume", "1.0") - self.config.set("speech", "hardware_device", "auto") + self.config.set("speech", "hardware_device", "/dev/ttyS0") self.config.set("speech", "hardware_baud_rate", "9600") self.config.add_section("sound") diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py index 0aa2356a..5a682001 100644 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py @@ -108,7 +108,7 @@ class command(config_command): "rate": "0.5", "pitch": "0.5", "volume": "1.0", - "hardware_device": "auto", + "hardware_device": "/dev/ttyS0", "hardware_baud_rate": "9600", "auto_read_incoming": "True", } diff --git a/src/fenrirscreenreader/core/settingsData.py b/src/fenrirscreenreader/core/settingsData.py index 5f738206..99b8e82b 100644 --- a/src/fenrirscreenreader/core/settingsData.py +++ b/src/fenrirscreenreader/core/settingsData.py @@ -28,7 +28,7 @@ settings_data = { "module": "", "voice": "en-us", "language": "", - "hardware_device": "auto", + "hardware_device": "/dev/ttyS0", "hardware_baud_rate": 9600, "auto_read_incoming": True, "read_numbers_as_digits": False, diff --git a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py index 8eb05b89..1df1ec00 100644 --- a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py +++ b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py @@ -4,9 +4,7 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. -import glob import os -import select import termios import threading import tty @@ -29,8 +27,6 @@ class SpeakQueue(Queue): class hardware_serial_driver(speech_driver): cancel_command = b"" default_baud_rate = 9600 - hardware_probe_command = b"" - hardware_probe_timeout = 0.2 def __init__(self): speech_driver.__init__(self) @@ -71,9 +67,9 @@ class hardware_serial_driver(speech_driver): def _clean_device_setting(self, device): if not isinstance(device, str): - return "auto" + return "" device = device.split("#", 1)[0].split(";", 1)[0].strip() - return device or "auto" + return device def shutdown(self): if not self._is_initialized: @@ -156,37 +152,17 @@ class hardware_serial_driver(speech_driver): ) def _open_serial_port(self): - auto_detect = self.device == "auto" - devices = self._resolve_devices(self.device) - if not devices: + if not self.device or self.device == "auto": self._debug( - "Hardware speech device not found", + "Hardware speech requires an explicit serial device", debug.DebugLevel.ERROR, on_any_level=True, ) return - for device in devices: - port = self._open_configured_serial_port(device) - if port is None: - continue - if not auto_detect: - self._activate_serial_port( - device, port, probe_matched=False - ) - return - if self._probe_serial_port(port): - self._activate_serial_port(device, port, probe_matched=True) - return - self._close_port(port) - - if auto_detect and self.hardware_probe_command: - self._debug( - "Hardware speech auto did not identify a synth; set " - "speech#hardware_device to the known serial device", - debug.DebugLevel.ERROR, - on_any_level=True, - ) + port = self._open_configured_serial_port(self.device) + if port is not None: + self._activate_serial_port(self.device, port) def _open_configured_serial_port(self, device): port = None @@ -212,60 +188,16 @@ class hardware_serial_driver(speech_driver): ) return None - def _activate_serial_port(self, device, port, probe_matched=False): + def _activate_serial_port(self, device, port): self.serial_port = port self.device = device - probe_status = "matched" if probe_matched else "not matched" self._debug( "Hardware speech device opened: " - f"{device}, baud_rate={self.baud_rate}, probe={probe_status}", + f"{device}, baud_rate={self.baud_rate}", debug.DebugLevel.INFO, on_any_level=True, ) - def _probe_serial_port(self, port): - if not self.hardware_probe_command: - return True - try: - self._drain_serial_input(port) - os.write(port, self.hardware_probe_command) - readable, _, _ = select.select( - [port], [], [], self.hardware_probe_timeout - ) - if not readable: - self._debug( - "Hardware speech probe got no response", - debug.DebugLevel.INFO, - on_any_level=True, - ) - return False - response = os.read(port, 256) - self._debug( - "Hardware speech probe response: " - f"{self._format_bytes_preview(response)}", - debug.DebugLevel.INFO, - on_any_level=True, - ) - return self._is_hardware_probe_response(response) - except OSError as error: - self._debug( - f"Hardware speech probe failed: {error}", - debug.DebugLevel.WARNING, - on_any_level=True, - ) - return False - - def _drain_serial_input(self, port): - while True: - readable, _, _ = select.select([port], [], [], 0) - if not readable: - return - if not os.read(port, 256): - return - - def _is_hardware_probe_response(self, response): - return bool(response) - def _close_serial_port(self): with self.lock: if self.serial_port is None: @@ -313,53 +245,6 @@ class hardware_serial_driver(speech_driver): on_any_level=True, ) - def _resolve_devices(self, device): - if device and device != "auto": - self._debug( - f"Hardware speech using configured device: {device}", - debug.DebugLevel.INFO, - on_any_level=True, - ) - 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 unique_matches: - if len(unique_matches) > 1: - self._debug( - "Hardware speech auto found multiple devices for " - f"{pattern}: {unique_matches}; probing in order", - debug.DebugLevel.WARNING, - on_any_level=True, - ) - self._debug( - "Hardware speech auto candidate devices: " - f"{unique_matches}", - debug.DebugLevel.INFO, - on_any_level=True, - ) - devices.extend(unique_matches) - return devices - def _termios_baud_rate(self, baud_rate): baud_name = f"B{baud_rate}" if hasattr(termios, baud_name): diff --git a/src/fenrirscreenreader/speechDriver/litetalkDriver.py b/src/fenrirscreenreader/speechDriver/litetalkDriver.py index cd6def28..6dadeda2 100644 --- a/src/fenrirscreenreader/speechDriver/litetalkDriver.py +++ b/src/fenrirscreenreader/speechDriver/litetalkDriver.py @@ -11,8 +11,6 @@ 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" @@ -28,10 +26,3 @@ class driver(hardware_serial_driver): def _setting_command(self, value, command): return b"\x01" + str(value).encode("ascii") + command - - def _is_hardware_probe_response(self, response): - response_text = response.decode("ascii", errors="ignore").lower() - return any( - token in response_text - for token in ("litetalk", "lite talk", "rpitalk", "doubletalk") - ) diff --git a/tests/unit/test_hardware_speech_drivers.py b/tests/unit/test_hardware_speech_drivers.py index fdfd9795..cada8144 100644 --- a/tests/unit/test_hardware_speech_drivers.py +++ b/tests/unit/test_hardware_speech_drivers.py @@ -1,6 +1,5 @@ import os import select -import termios import time from unittest.mock import ANY from unittest.mock import Mock @@ -105,20 +104,8 @@ def test_litetalk_driver_writes_settings_and_cancel(serial_pair): speech_driver.shutdown() -def test_configured_device_supports_classic_serial( - monkeypatch, serial_pair -): +def test_configured_device_supports_classic_serial(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(slave_name)) try: @@ -142,272 +129,7 @@ def test_configured_device_strips_inline_comment(serial_pair): speech_driver.shutdown() -def test_auto_device_detection_prefers_probe_response(monkeypatch): - opened_ports = [] - closed_ports = [] - writes = {} - - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", - lambda pattern: ["/dev/ttyUSB0", "/dev/ttyUSB1"] - if pattern == "/dev/ttyUSB*" - else [], - ) - - def fake_open(device, flags): - port = 100 + len(opened_ports) - opened_ports.append((device, port)) - return port - - def fake_write(port, data): - writes.setdefault(port, b"") - writes[port] += data - return len(data) - - 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 - - def fake_read(port, size): - if port == 101: - return b"RPItalk 1.3\r" - return b"" - - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.open", - fake_open, - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.close", - lambda port: closed_ports.append(port), - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.write", - fake_write, - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.read", - fake_read, - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.select.select", - fake_select, - ) - 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() - speech_driver.initialize(build_environment("auto")) - try: - assert opened_ports == [("/dev/ttyUSB0", 100), ("/dev/ttyUSB1", 101)] - assert writes == {100: b"\x01I", 101: b"\x01I"} - assert closed_ports == [100] - assert speech_driver.device == "/dev/ttyUSB1" - finally: - speech_driver.shutdown() - - -def test_auto_device_detection_fails_without_probe_response( - monkeypatch -): - opened_ports = [] - closed_ports = [] - - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", - lambda pattern: ["/dev/ttyUSB0", "/dev/ttyUSB1"] - if pattern == "/dev/ttyUSB*" - else [], - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.open", - lambda device, flags: opened_ports.append(device) or len(opened_ports), - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.close", - lambda port: closed_ports.append(port), - ) - 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_ports == ["/dev/ttyUSB0", "/dev/ttyUSB1"] - assert closed_ports == [1, 2] - 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 = [] - - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", - lambda pattern: ["/dev/ttyUSB0", "/dev/ttyUSB1"] - if pattern == "/dev/ttyUSB*" - else [], - ) - - def fake_open(device, flags): - port = 100 + len(opened_ports) - opened_ports.append((device, port)) - return port - - def fake_tcgetattr(port): - if port == 100: - raise termios.error(5, "Input/output error") - return [0, 0, 0, 0, 0, 0, [0] * 32] - - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.open", - fake_open, - ) - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.close", - lambda port: closed_ports.append(port), - ) - 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", - fake_tcgetattr, - ) - 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_ports == [("/dev/ttyUSB0", 100), ("/dev/ttyUSB1", 101)] - assert closed_ports == [100, 101] - assert speech_driver.device == "auto" - - -def test_auto_device_detection_fails_when_no_serial_device(monkeypatch): - monkeypatch.setattr( - "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", - lambda pattern: [], - ) +def test_auto_device_is_rejected(): speech_driver = litetalkDriver.driver() with pytest.raises(RuntimeError, match="hardware speech device"): @@ -415,7 +137,7 @@ def test_auto_device_detection_fails_when_no_serial_device(monkeypatch): debug_manager = speech_driver.env["runtime"]["DebugManager"] debug_manager.write_debug_out.assert_called_with( - "Hardware speech device not found", + "Hardware speech requires an explicit serial device", ANY, on_any_level=True, )