From 604221a29d4af61ec7f4591dbb1f125b1dedd176 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 23 May 2026 18:23:58 -0400 Subject: [PATCH] Attempt to make auto at least somewhat more reliable. Recommend that device be explicitly set if possible. --- README.md | 6 ++- config/settings/settings.conf | 5 ++- docs/fenrir.adoc | 8 ++-- docs/user.md | 11 +++--- docs/user.txt | 4 +- .../speechDriver/hardwareSerialDriver.py | 29 ++++++-------- tests/unit/test_hardware_speech_drivers.py | 39 +++++++++---------- 7 files changed, 49 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 756b5bf9..c89ca157 100644 --- a/README.md +++ b/README.md @@ -460,13 +460,15 @@ setting [parameters] - `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3") - `speech#module=module_name` - TTS module (e.g., "espeak-ng") - `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver/dectalkDriver/litetalkDriver/doubletalkDriver/tripletalkDriver) -- `speech#hardware_device=auto` - Hardware synth serial device for dectalkDriver/litetalkDriver +- `speech#hardware_device=/dev/ttyS0` - Hardware synth serial device for dectalkDriver/litetalkDriver - `speech#hardware_baud_rate=9600` - Hardware synth serial baud rate - `speech#history_size=50` - Number of spoken items kept in runtime speech history 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. +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#auto_read_incoming=True/False` - Auto-read new text *Sound Settings:* diff --git a/config/settings/settings.conf b/config/settings/settings.conf index b80ca2d7..8f96cbe4 100644 --- a/config/settings/settings.conf +++ b/config/settings/settings.conf @@ -79,12 +79,13 @@ 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. -# auto checks /dev/ttyACM* first, then /dev/ttyUSB*. +# 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. # Examples: # hardware_device=/dev/ttyACM0 # RPITalk USB gadget mode # hardware_device=/dev/ttyUSB0 # USB serial adapter # hardware_device=/dev/ttyS0 # built-in serial port -hardware_device=auto +hardware_device=/dev/ttyS0 # Serial baud rate for hardware speech synthesizers. hardware_baud_rate=9600 diff --git a/docs/fenrir.adoc b/docs/fenrir.adoc index e212fa75..920d6ad9 100644 --- a/docs/fenrir.adoc +++ b/docs/fenrir.adoc @@ -1684,15 +1684,15 @@ the pico module: language=de-DE .... -Hardware speech drivers use a serial device. The default `+auto+` checks -`+/dev/ttyACM*+` first, then `+/dev/ttyUSB*+`. Set an explicit path for -stable systems. +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_device=auto 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 f9f5602c..6df46dcf 100644 --- a/docs/user.md +++ b/docs/user.md @@ -101,7 +101,7 @@ driver=speechdDriver rate=0.5 pitch=0.5 volume=1.0 -hardware_device=auto +hardware_device=/dev/ttyS0 hardware_baud_rate=9600 history_size=50 @@ -341,10 +341,11 @@ Fenrir automatically detects and provides audio feedback for progress indicators - **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech - **tripletalkDriver** - Serial TripleTalk-compatible hardware speech -For hardware speech, set `speech#hardware_device` to `auto` or 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`. `doubletalkDriver` targets +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 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 34c15b37..41d73e83 100644 --- a/docs/user.txt +++ b/docs/user.txt @@ -927,11 +927,11 @@ 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. The default ''auto'' checks /dev/ttyACM* first, then /dev/ttyUSB*. Set an explicit path for stable systems. - hardware_device=auto +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_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/speechDriver/hardwareSerialDriver.py b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py index a32cdd8b..12c66d90 100644 --- a/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py +++ b/src/fenrirscreenreader/speechDriver/hardwareSerialDriver.py @@ -166,8 +166,6 @@ class hardware_serial_driver(speech_driver): ) return - fallback_device = None - fallback_port = None for device in devices: port = self._open_configured_serial_port(device) if port is None: @@ -178,28 +176,20 @@ class hardware_serial_driver(speech_driver): ) return if self._probe_serial_port(port): - if fallback_port is not None: - self._close_port(fallback_port) self._activate_serial_port(device, port, probe_matched=True) return - if fallback_port is None: - fallback_device = device - fallback_port = port - else: - self._close_port(port) + self._close_port(port) - if fallback_port is not None: + if auto_detect and self.hardware_probe_command: self._debug( - "Hardware speech probe did not identify a synth; " - f"falling back to {fallback_device}", - debug.DebugLevel.WARNING, + "Hardware speech auto did not identify a synth; set " + "speech#hardware_device to the known serial device", + debug.DebugLevel.ERROR, on_any_level=True, ) - self._activate_serial_port( - fallback_device, fallback_port, probe_matched=False - ) def _open_configured_serial_port(self, device): + port = None try: port = os.open(device, os.O_RDWR | os.O_NOCTTY) tty.setraw(port) @@ -214,6 +204,7 @@ class hardware_serial_driver(speech_driver): termios.tcsetattr(port, termios.TCSANOW, attrs) return port except (OSError, termios.error) as error: + self._close_port(port) self._debug( f"Hardware speech device open failed: {device}: {error}", debug.DebugLevel.ERROR, @@ -331,7 +322,11 @@ class hardware_serial_driver(speech_driver): ) return [device] devices = [] - for pattern in ("/dev/ttyACM*", "/dev/ttyUSB*", "/dev/ttyS*"): + for pattern in ( + "/dev/serial/by-id/*", + "/dev/ttyACM*", + "/dev/ttyUSB*", + ): matches = sorted(glob.glob(pattern)) self._debug( f"Hardware speech auto scan {pattern}: {matches}", diff --git a/tests/unit/test_hardware_speech_drivers.py b/tests/unit/test_hardware_speech_drivers.py index 100b4947..0e7c36c5 100644 --- a/tests/unit/test_hardware_speech_drivers.py +++ b/tests/unit/test_hardware_speech_drivers.py @@ -105,7 +105,7 @@ def test_litetalk_driver_writes_settings_and_cancel(serial_pair): speech_driver.shutdown() -def test_auto_device_detection_includes_classic_serial( +def test_configured_device_supports_classic_serial( monkeypatch, serial_pair ): master_fd, slave_name = serial_pair @@ -120,11 +120,11 @@ def test_auto_device_detection_includes_classic_serial( fake_glob, ) speech_driver = litetalkDriver.driver() - speech_driver.initialize(build_environment("auto")) + speech_driver.initialize(build_environment(slave_name)) try: assert speech_driver.device == slave_name speech_driver.speak("Serial") - assert read_available(master_fd, 9) == b"\x01ISerial\r" + assert read_available(master_fd, 7) == b"Serial\r" finally: speech_driver.shutdown() @@ -219,7 +219,7 @@ def test_auto_device_detection_prefers_probe_response(monkeypatch): speech_driver.shutdown() -def test_auto_device_detection_falls_back_without_probe_response( +def test_auto_device_detection_fails_without_probe_response( monkeypatch ): opened_ports = [] @@ -265,13 +265,12 @@ def test_auto_device_detection_falls_back_without_probe_response( ) speech_driver = litetalkDriver.driver() - speech_driver.initialize(build_environment("auto")) - try: - assert opened_ports == ["/dev/ttyUSB0", "/dev/ttyUSB1"] - assert closed_ports == [2] - assert speech_driver.device == "/dev/ttyUSB0" - finally: - speech_driver.shutdown() + 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_skips_termios_failures(monkeypatch): @@ -280,10 +279,8 @@ def test_auto_device_detection_skips_termios_failures(monkeypatch): monkeypatch.setattr( "fenrirscreenreader.speechDriver.hardwareSerialDriver.glob.glob", - lambda pattern: ["/dev/ttyUSB0"] + lambda pattern: ["/dev/ttyUSB0", "/dev/ttyUSB1"] if pattern == "/dev/ttyUSB*" - else ["/dev/ttyS0"] - if pattern == "/dev/ttyS*" else [], ) @@ -293,7 +290,7 @@ def test_auto_device_detection_skips_termios_failures(monkeypatch): return port def fake_tcgetattr(port): - if port == 101: + if port == 100: raise termios.error(5, "Input/output error") return [0, 0, 0, 0, 0, 0, [0] * 32] @@ -331,12 +328,12 @@ def test_auto_device_detection_skips_termios_failures(monkeypatch): ) speech_driver = litetalkDriver.driver() - speech_driver.initialize(build_environment("auto")) - try: - assert opened_ports == [("/dev/ttyUSB0", 100), ("/dev/ttyS0", 101)] - assert speech_driver.device == "/dev/ttyUSB0" - finally: - speech_driver.shutdown() + 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):