Removed auto as a hardware synth device option. It was too flakey.

This commit is contained in:
Storm Dragon
2026-05-23 18:58:55 -04:00
parent 618987546a
commit ce43d64e77
11 changed files with 20 additions and 429 deletions
+1 -2
View File
@@ -467,8 +467,7 @@ setting <action> [parameters]
USB hardware synths are supported only when Linux exposes them as a serial tty 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 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 device would require a separate USB protocol driver. Use an explicit
`speech#hardware_device` path for stable hardware speech; `auto` is limited to `speech#hardware_device` path for hardware speech.
devices that respond to a driver-specific probe.
- `speech#auto_read_incoming=True/False` - Auto-read new text - `speech#auto_read_incoming=True/False` - Auto-read new text
*Sound Settings:* *Sound Settings:*
+1 -2
View File
@@ -79,8 +79,7 @@ volume=1.0
# Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver. # Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver.
# USB serial devices are supported if Linux exposes them as /dev/ttyACM* # 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. # 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 # Set an explicit device for hardware speech.
# that respond to a driver-specific probe and may fail on silent synths.
# Examples: # Examples:
# hardware_device=/dev/ttyACM0 # RPITalk USB gadget mode # hardware_device=/dev/ttyACM0 # RPITalk USB gadget mode
# hardware_device=/dev/ttyUSB0 # USB serial adapter # hardware_device=/dev/ttyUSB0 # USB serial adapter
+1 -4
View File
@@ -1684,15 +1684,12 @@ the pico module:
language=de-DE language=de-DE
.... ....
Hardware speech drivers use a serial device. Set an explicit path for stable Hardware speech drivers use a serial device. Set an explicit path.
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/ttyACM0
hardware_device=/dev/ttyUSB0 hardware_device=/dev/ttyUSB0
hardware_device=/dev/ttyS0 hardware_device=/dev/ttyS0
hardware_device=auto
.... ....
Hardware speech drivers use 9600 baud by default. Hardware speech drivers use 9600 baud by default.
+1 -2
View File
@@ -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. For hardware speech, set `speech#hardware_device` to an explicit serial path.
RPITalk gadget mode usually appears as `/dev/ttyACM0`; USB serial adapters RPITalk gadget mode usually appears as `/dev/ttyACM0`; USB serial adapters
usually appear as `/dev/ttyUSB0`; built-in serial ports may be `/dev/ttyS0`. 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 The default baud rate is `9600`. `doubletalkDriver` targets
driver-specific probe and may fail on silent synths. `doubletalkDriver` targets
DoubleTalk LT-style serial devices, not the internal DoubleTalk PC ISA card. 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 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 `/dev/ttyACM0` or `/dev/ttyUSB0`; USB-only models with no tty device need a
+1 -2
View File
@@ -927,11 +927,10 @@ Select the language you want Fenrir to use.
language=english-us language=english-us
Values: Text, see your TTS synths documentation what is available. 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/ttyACM0
hardware_device=/dev/ttyUSB0 hardware_device=/dev/ttyUSB0
hardware_device=/dev/ttyS0 hardware_device=/dev/ttyS0
hardware_device=auto
Hardware speech drivers use 9600 baud by default. Hardware speech drivers use 9600 baud by default.
hardware_baud_rate=9600 hardware_baud_rate=9600
@@ -127,7 +127,7 @@ class config_command:
self.config.set("speech", "rate", "0.75") self.config.set("speech", "rate", "0.75")
self.config.set("speech", "pitch", "0.5") self.config.set("speech", "pitch", "0.5")
self.config.set("speech", "volume", "1.0") 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.set("speech", "hardware_baud_rate", "9600")
self.config.add_section("sound") self.config.add_section("sound")
@@ -108,7 +108,7 @@ class command(config_command):
"rate": "0.5", "rate": "0.5",
"pitch": "0.5", "pitch": "0.5",
"volume": "1.0", "volume": "1.0",
"hardware_device": "auto", "hardware_device": "/dev/ttyS0",
"hardware_baud_rate": "9600", "hardware_baud_rate": "9600",
"auto_read_incoming": "True", "auto_read_incoming": "True",
} }
+1 -1
View File
@@ -28,7 +28,7 @@ settings_data = {
"module": "", "module": "",
"voice": "en-us", "voice": "en-us",
"language": "", "language": "",
"hardware_device": "auto", "hardware_device": "/dev/ttyS0",
"hardware_baud_rate": 9600, "hardware_baud_rate": 9600,
"auto_read_incoming": True, "auto_read_incoming": True,
"read_numbers_as_digits": False, "read_numbers_as_digits": False,
@@ -4,9 +4,7 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
import glob
import os import os
import select
import termios import termios
import threading import threading
import tty import tty
@@ -29,8 +27,6 @@ class SpeakQueue(Queue):
class hardware_serial_driver(speech_driver): class hardware_serial_driver(speech_driver):
cancel_command = b"" cancel_command = b""
default_baud_rate = 9600 default_baud_rate = 9600
hardware_probe_command = b""
hardware_probe_timeout = 0.2
def __init__(self): def __init__(self):
speech_driver.__init__(self) speech_driver.__init__(self)
@@ -71,9 +67,9 @@ class hardware_serial_driver(speech_driver):
def _clean_device_setting(self, device): def _clean_device_setting(self, device):
if not isinstance(device, str): if not isinstance(device, str):
return "auto" return ""
device = device.split("#", 1)[0].split(";", 1)[0].strip() device = device.split("#", 1)[0].split(";", 1)[0].strip()
return device or "auto" return device
def shutdown(self): def shutdown(self):
if not self._is_initialized: if not self._is_initialized:
@@ -156,37 +152,17 @@ class hardware_serial_driver(speech_driver):
) )
def _open_serial_port(self): def _open_serial_port(self):
auto_detect = self.device == "auto" if not self.device or self.device == "auto":
devices = self._resolve_devices(self.device)
if not devices:
self._debug( self._debug(
"Hardware speech device not found", "Hardware speech requires an explicit serial device",
debug.DebugLevel.ERROR, debug.DebugLevel.ERROR,
on_any_level=True, on_any_level=True,
) )
return return
for device in devices: port = self._open_configured_serial_port(self.device)
port = self._open_configured_serial_port(device) if port is not None:
if port is None: self._activate_serial_port(self.device, port)
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,
)
def _open_configured_serial_port(self, device): def _open_configured_serial_port(self, device):
port = None port = None
@@ -212,60 +188,16 @@ class hardware_serial_driver(speech_driver):
) )
return None return None
def _activate_serial_port(self, device, port, probe_matched=False): def _activate_serial_port(self, device, port):
self.serial_port = port self.serial_port = port
self.device = device self.device = device
probe_status = "matched" if probe_matched else "not matched"
self._debug( self._debug(
"Hardware speech device opened: " "Hardware speech device opened: "
f"{device}, baud_rate={self.baud_rate}, probe={probe_status}", f"{device}, baud_rate={self.baud_rate}",
debug.DebugLevel.INFO, debug.DebugLevel.INFO,
on_any_level=True, 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): def _close_serial_port(self):
with self.lock: with self.lock:
if self.serial_port is None: if self.serial_port is None:
@@ -313,53 +245,6 @@ class hardware_serial_driver(speech_driver):
on_any_level=True, 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): def _termios_baud_rate(self, baud_rate):
baud_name = f"B{baud_rate}" baud_name = f"B{baud_rate}"
if hasattr(termios, baud_name): if hasattr(termios, baud_name):
@@ -11,8 +11,6 @@ from fenrirscreenreader.speechDriver.hardwareSerialDriver import (
class driver(hardware_serial_driver): class driver(hardware_serial_driver):
cancel_command = b"\x18" cancel_command = b"\x18"
hardware_probe_command = b"\x01I"
hardware_probe_timeout = 3.5
def _speak_bytes(self, text): def _speak_bytes(self, text):
return self._clean_text(text).encode("ascii", errors="replace") + b"\r" 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): def _setting_command(self, value, command):
return b"\x01" + str(value).encode("ascii") + 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")
)
+3 -281
View File
@@ -1,6 +1,5 @@
import os import os
import select import select
import termios
import time import time
from unittest.mock import ANY from unittest.mock import ANY
from unittest.mock import Mock from unittest.mock import Mock
@@ -105,20 +104,8 @@ def test_litetalk_driver_writes_settings_and_cancel(serial_pair):
speech_driver.shutdown() speech_driver.shutdown()
def test_configured_device_supports_classic_serial( def test_configured_device_supports_classic_serial(serial_pair):
monkeypatch, serial_pair
):
master_fd, slave_name = 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 = litetalkDriver.driver()
speech_driver.initialize(build_environment(slave_name)) speech_driver.initialize(build_environment(slave_name))
try: try:
@@ -142,272 +129,7 @@ def test_configured_device_strips_inline_comment(serial_pair):
speech_driver.shutdown() speech_driver.shutdown()
def test_auto_device_detection_prefers_probe_response(monkeypatch): def test_auto_device_is_rejected():
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: [],
)
speech_driver = litetalkDriver.driver() speech_driver = litetalkDriver.driver()
with pytest.raises(RuntimeError, match="hardware speech device"): 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 = speech_driver.env["runtime"]["DebugManager"]
debug_manager.write_debug_out.assert_called_with( debug_manager.write_debug_out.assert_called_with(
"Hardware speech device not found", "Hardware speech requires an explicit serial device",
ANY, ANY,
on_any_level=True, on_any_level=True,
) )