More attempts at getting this usb dectalk working.

This commit is contained in:
Storm Dragon
2026-07-26 20:18:17 -04:00
parent 45bd33f756
commit c863245c24
11 changed files with 531 additions and 21 deletions
+18 -8
View File
@@ -81,13 +81,16 @@ Fenrir is a Linux screen reader. Linux is the only officially supported platform
- espeak or espeak-ng (or any TTS command) - espeak or espeak-ng (or any TTS command)
3. **dectalkDriver** - Serial DECtalk-compatible hardware speech driver 3. **dectalkDriver** - Serial DECtalk-compatible hardware speech driver
- RPITalk gadget mode or a DECtalk-compatible serial device - RPITalk gadget mode or a DECtalk-compatible serial device
4. **litetalkDriver** - Serial LiteTalk-compatible hardware speech driver 4. **dectalkUsbDriver** - Native Access Solutions DECtalk USB driver
- USB ID `0dd0:2001`
- PyUSB and a libusb backend
5. **litetalkDriver** - Serial LiteTalk-compatible hardware speech driver
- RPITalk gadget mode or a LiteTalk-compatible serial device - RPITalk gadget mode or a LiteTalk-compatible serial device
5. **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech driver 6. **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech driver
- DoubleTalk LT; does not support the internal DoubleTalk PC card - DoubleTalk LT; does not support the internal DoubleTalk PC card
6. **tripletalkDriver** - Serial TripleTalk-compatible hardware speech driver 7. **tripletalkDriver** - Serial TripleTalk-compatible hardware speech driver
- External DB9 serial TripleTalk devices, or USB models that expose a tty serial device - External DB9 serial TripleTalk devices, or USB models that expose a tty serial device
7. **debugDriver** - Debug speech driver for testing 8. **debugDriver** - Debug speech driver for testing
- No dependencies - No dependencies
@@ -459,15 +462,22 @@ setting <action> [parameters]
- `speech#volume=0.1-1.0` - Speech volume - `speech#volume=0.1-1.0` - Speech volume
- `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3") - `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3")
- `speech#module=module_name` - TTS module (e.g., "espeak-ng") - `speech#module=module_name` - TTS module (e.g., "espeak-ng")
- `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver/dectalkDriver/litetalkDriver/doubletalkDriver/tripletalkDriver) - `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver/dectalkDriver/dectalkUsbDriver/litetalkDriver/doubletalkDriver/tripletalkDriver)
- `speech#hardware_device=/dev/ttyS0` - 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#hardware_baud_rate=9600` - Hardware synth serial baud rate
- `speech#history_size=50` - Number of spoken items kept in runtime speech history - `speech#history_size=50` - Number of spoken items kept in runtime speech history
The native Access Solutions DECtalk USB, USB ID `0dd0:2001`, uses
`dectalkUsbDriver`. It is autodetected and does not use
`speech#hardware_device` or `speech#hardware_baud_rate`. Install PyUSB and a
libusb backend first. From a source checkout, use
`python3 -m pip install '.[dectalk_usb]'`; distributions may package PyUSB as
`python3-usb` or `python-pyusb`. Root-run Fenrir can access the device directly.
A non-root Fenrir process needs a local udev rule granting its account
read/write access to `0dd0:2001`; do not make the device world-writable. Other
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`. Use an explicit
device would require a separate USB protocol driver. Use an explicit `speech#hardware_device` path for serial hardware speech.
`speech#hardware_device` path for hardware speech.
- `speech#auto_read_incoming=True/False` - Auto-read new text - `speech#auto_read_incoming=True/False` - Auto-read new text
*Sound Settings:* *Sound Settings:*
+4 -1
View File
@@ -36,10 +36,12 @@ progress_monitoring=True
enabled=True enabled=True
# Select speech driver, options are speechdDriver, genericDriver, # Select speech driver, options are speechdDriver, genericDriver,
# dectalkDriver, litetalkDriver, doubletalkDriver, or tripletalkDriver: # dectalkDriver, dectalkUsbDriver, litetalkDriver, doubletalkDriver,
# or tripletalkDriver:
driver=speechdDriver driver=speechdDriver
#driver=genericDriver #driver=genericDriver
#driver=dectalkDriver #driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver #driver=litetalkDriver
#driver=doubletalkDriver #driver=doubletalkDriver
#driver=tripletalkDriver #driver=tripletalkDriver
@@ -77,6 +79,7 @@ volume=1.0
# Hardware speech synthesizer serial device. # Hardware speech synthesizer serial device.
# Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver. # Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver.
# dectalkUsbDriver autodetects the Access Solutions device and ignores this.
# 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 hardware speech. # Set an explicit device for hardware speech.
+2
View File
@@ -242,6 +242,8 @@ genericDriver - Command-line TTS (espeak, etc.)
.IP \[bu] 4 .IP \[bu] 4
dectalkDriver - DECtalk-compatible serial hardware speech dectalkDriver - DECtalk-compatible serial hardware speech
.IP \[bu] 4 .IP \[bu] 4
dectalkUsbDriver - Native Access Solutions DECtalk USB hardware speech
.IP \[bu] 4
litetalkDriver - LiteTalk-compatible serial hardware speech litetalkDriver - LiteTalk-compatible serial hardware speech
.IP \[bu] 4 .IP \[bu] 4
doubletalkDriver - DoubleTalk LT-compatible serial hardware speech doubletalkDriver - DoubleTalk LT-compatible serial hardware speech
+12 -5
View File
@@ -1551,9 +1551,11 @@ enabled=True
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
# Select speech driver, options are speechdDriver (default), # Select speech driver, options are speechdDriver (default),
genericDriver, dectalkDriver, litetalkDriver, doubletalkDriver or tripletalkDriver: driver=speechdDriver genericDriver, dectalkDriver, dectalkUsbDriver, litetalkDriver,
doubletalkDriver or tripletalkDriver: driver=speechdDriver
#driver=genericDriver #driver=genericDriver
#driver=dectalkDriver #driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver #driver=litetalkDriver
#driver=doubletalkDriver #driver=doubletalkDriver
#driver=tripletalkDriver #driver=tripletalkDriver
@@ -1684,7 +1686,7 @@ the pico module:
language=de-DE language=de-DE
.... ....
Hardware speech drivers use a serial device. Set an explicit path. Serial hardware speech drivers use a serial device. Set an explicit path.
.... ....
hardware_device=/dev/ttyACM0 hardware_device=/dev/ttyACM0
@@ -1700,9 +1702,14 @@ hardware_baud_rate=9600
The `+doubletalkDriver+` targets DoubleTalk LT-style serial devices. It does The `+doubletalkDriver+` targets DoubleTalk LT-style serial devices. It does
not support the internal DoubleTalk PC ISA card. not support the internal DoubleTalk PC ISA card.
USB hardware speech synthesizers are supported only when Linux exposes them as The `+dectalkUsbDriver+` autodetects the native Access Solutions DECtalk USB,
a serial tty such as `+/dev/ttyACM0+` or `+/dev/ttyUSB0+`. USB-only TripleTalk USB ID `+0dd0:2001+`. It requires PyUSB and a libusb backend and ignores the
models with no tty device need a separate driver. serial device and baud-rate settings. From a source checkout, install the USB
dependencies with `+python3 -m pip install '.[dectalk_usb]'+`. A non-root
Fenrir process needs a local udev rule granting its account read/write access
to `+0dd0:2001+`; do not make the device world-writable. Other USB hardware
speech synthesizers are supported only when Linux exposes them as a serial tty
such as `+/dev/ttyACM0+` or `+/dev/ttyUSB0+`.
Read new text as it occurs auto_read_incoming=True Values: on=`+True+`, Read new text as it occurs auto_read_incoming=True Values: on=`+True+`,
off=`+False+` off=`+False+`
+10 -4
View File
@@ -340,18 +340,24 @@ status formats, which change too frequently to support reliably.
- **speechdDriver** - Speech-dispatcher (recommended) - **speechdDriver** - Speech-dispatcher (recommended)
- **genericDriver** - Command-line TTS (espeak, etc.) - **genericDriver** - Command-line TTS (espeak, etc.)
- **dectalkDriver** - Serial DECtalk-compatible hardware speech - **dectalkDriver** - Serial DECtalk-compatible hardware speech
- **dectalkUsbDriver** - Native Access Solutions DECtalk USB (`0dd0:2001`)
- **litetalkDriver** - Serial LiteTalk-compatible hardware speech - **litetalkDriver** - Serial LiteTalk-compatible hardware speech
- **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech - **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech
- **tripletalkDriver** - Serial TripleTalk-compatible hardware speech - **tripletalkDriver** - Serial TripleTalk-compatible hardware speech
For hardware speech, set `speech#hardware_device` to an explicit serial path. For serial hardware speech, set `speech#hardware_device` to an explicit 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`. `doubletalkDriver` targets The default baud rate is `9600`. `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 The `dectalkUsbDriver` autodetects the native Access Solutions DECtalk USB and
`/dev/ttyACM0` or `/dev/ttyUSB0`; USB-only models with no tty device need a ignores both serial settings. It requires PyUSB and a libusb backend. From a
separate driver. source checkout, install them with
`python3 -m pip install '.[dectalk_usb]'`. A non-root Fenrir process also needs
a local udev rule granting its account read/write access to USB ID
`0dd0:2001`; do not make the device world-writable. USB TripleTalk devices work
only if Linux exposes them as a serial tty such as `/dev/ttyACM0` or
`/dev/ttyUSB0`.
### Sound Drivers ### Sound Drivers
- **genericDriver** - Sox-based (default) - **genericDriver** - Sox-based (default)
+5 -3
View File
@@ -878,10 +878,11 @@ Turn speech on or off:
enabled=True enabled=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
# Select speech driver, options are speechdDriver (default), genericDriver, dectalkDriver, litetalkDriver, doubletalkDriver or tripletalkDriver: # Select speech driver, options are speechdDriver (default), genericDriver, dectalkDriver, dectalkUsbDriver, litetalkDriver, doubletalkDriver or tripletalkDriver:
driver=speechdDriver driver=speechdDriver
#driver=genericDriver #driver=genericDriver
#driver=dectalkDriver #driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver #driver=litetalkDriver
#driver=doubletalkDriver #driver=doubletalkDriver
#driver=tripletalkDriver #driver=tripletalkDriver
@@ -894,6 +895,7 @@ Available Drivers:
* ''genericDriver'' using the generic driver, for Fenrir <1.5 this is not available * ''genericDriver'' using the generic driver, for Fenrir <1.5 this is not available
* ''speechdDriver'' using speech-dispatcher, for Fenrir <1.5 just use ''speechd'' * ''speechdDriver'' using speech-dispatcher, for Fenrir <1.5 just use ''speechd''
* ''dectalkDriver'' using DECtalk-compatible serial hardware or RPITalk * ''dectalkDriver'' using DECtalk-compatible serial hardware or RPITalk
* ''dectalkUsbDriver'' using the native Access Solutions DECtalk USB
* ''litetalkDriver'' using LiteTalk-compatible serial hardware or RPITalk * ''litetalkDriver'' using LiteTalk-compatible serial hardware or RPITalk
* ''doubletalkDriver'' using DoubleTalk LT-compatible serial hardware * ''doubletalkDriver'' using DoubleTalk LT-compatible serial hardware
* ''tripletalkDriver'' using TripleTalk-compatible serial hardware * ''tripletalkDriver'' using TripleTalk-compatible serial hardware
@@ -927,7 +929,7 @@ 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. Serial 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
@@ -936,7 +938,7 @@ Hardware speech drivers use 9600 baud by default.
hardware_baud_rate=9600 hardware_baud_rate=9600
The doubletalkDriver targets DoubleTalk LT-style serial devices. It does not support the internal DoubleTalk PC ISA card. The doubletalkDriver targets DoubleTalk LT-style serial devices. It does not support the internal DoubleTalk PC ISA card.
USB hardware speech synthesizers are supported only when Linux exposes them as a serial tty such as /dev/ttyACM0 or /dev/ttyUSB0. USB-only TripleTalk models with no tty device need a separate driver. The dectalkUsbDriver autodetects the native Access Solutions DECtalk USB, USB ID 0dd0:2001. It requires PyUSB and a libusb backend and ignores the serial device and baud-rate settings. Other USB hardware speech synthesizers are supported only when Linux exposes them as a serial tty such as /dev/ttyACM0 or /dev/ttyUSB0.
Read new text as it occurs Read new text as it occurs
auto_read_incoming=True auto_read_incoming=True
+1
View File
@@ -106,6 +106,7 @@ setup(
"pyte>=0.7.0", "pyte>=0.7.0",
], ],
extras_require={ extras_require={
"dectalk_usb": ["pyusb>=1.2.1"],
"x11": ["python-xlib>=0.33"], "x11": ["python-xlib>=0.33"],
}, },
) )
@@ -521,6 +521,7 @@ class SettingsManager:
"speechdDriver", "speechdDriver",
"genericDriver", "genericDriver",
"dectalkDriver", "dectalkDriver",
"dectalkUsbDriver",
"doubletalkDriver", "doubletalkDriver",
"litetalkDriver", "litetalkDriver",
"tripletalkDriver", "tripletalkDriver",
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import threading
from fenrirscreenreader.core import debug
from fenrirscreenreader.speechDriver import dectalkDriver
class driver(dectalkDriver.driver):
vendor_id = 0x0DD0
product_id = 0x2001
interface = 0
speech_endpoint = 0x02
immediate_endpoint = 0x04
packet_size = 64
write_timeout = 1000
def initialize(self, environment):
self.env = environment
self._is_initialized = False
self.usb_device = None
self.usb_core = None
self.usb_util = None
self.immediate_lock = threading.Lock()
self._debug(
"DECtalk USB initialize: requested_device=0dd0:2001",
debug.DebugLevel.INFO,
on_any_level=True,
)
try:
self.usb_core, self.usb_util = self._load_usb_modules()
except ImportError as error:
self._debug(
f"DECtalk USB requires PyUSB: {error}",
debug.DebugLevel.ERROR,
on_any_level=True,
)
raise RuntimeError("DECtalk USB requires PyUSB") from error
try:
usb_device = self.usb_core.find(
idVendor=self.vendor_id,
idProduct=self.product_id,
)
except (
self.usb_core.NoBackendError,
self.usb_core.USBError,
OSError,
) as error:
self._debug(
f"DECtalk USB discovery failed: {error}",
debug.DebugLevel.ERROR,
on_any_level=True,
)
raise RuntimeError(
f"DECtalk USB discovery failed: {error}"
) from error
if usb_device is None:
self._debug(
"DECtalk USB device 0dd0:2001 was not found",
debug.DebugLevel.ERROR,
on_any_level=True,
)
raise RuntimeError("DECtalk USB device 0dd0:2001 was not found")
try:
try:
usb_device.get_active_configuration()
except self.usb_core.USBError:
usb_device.set_configuration()
self.usb_util.claim_interface(usb_device, self.interface)
except (self.usb_core.USBError, OSError) as error:
self._dispose_usb_device(usb_device)
self._debug(
f"DECtalk USB open failed: {error}",
debug.DebugLevel.ERROR,
on_any_level=True,
)
raise RuntimeError(f"DECtalk USB open failed: {error}") from error
self.usb_device = usb_device
self._is_initialized = True
self._stop_worker = False
self.worker_thread = threading.Thread(
target=self._worker,
daemon=True,
)
self.worker_thread.start()
self._debug(
"DECtalk USB device opened: 0dd0:2001",
debug.DebugLevel.INFO,
on_any_level=True,
)
def shutdown(self):
if not self._is_initialized:
return
self._stop_worker = True
self.clear_buffer()
self.text_queue.put(None)
if self.worker_thread:
self.worker_thread.join(timeout=0.5)
try:
with self.lock, self.immediate_lock:
usb_device = self.usb_device
self.usb_device = None
if usb_device is None:
return
try:
self.usb_util.release_interface(
usb_device,
self.interface,
)
except (self.usb_core.USBError, OSError) as error:
self._debug(
f"DECtalk USB interface release failed: {error}",
debug.DebugLevel.WARNING,
)
self._dispose_usb_device(usb_device)
finally:
self._is_initialized = False
def cancel(self):
if not self._is_initialized:
return
self.clear_buffer()
self._write_endpoint(
self.immediate_endpoint,
b"\x06",
"cancel",
write_lock=self.immediate_lock,
)
def _write_bytes(self, data, description="data"):
self._write_endpoint(self.speech_endpoint, data, description)
def _write_endpoint(
self,
endpoint,
data,
description,
write_lock=None,
):
if not data:
return
if write_lock is None:
write_lock = self.lock
with write_lock:
if self.usb_device is None:
return
try:
total_written = 0
while total_written < len(data):
chunk = data[
total_written : total_written + self.packet_size
]
bytes_written = self.usb_device.write(
endpoint,
chunk,
timeout=self.write_timeout,
)
if bytes_written != len(chunk):
raise OSError(
"USB write returned "
f"{bytes_written} of {len(chunk)} bytes"
)
total_written += bytes_written
preview = self._format_bytes_preview(data)
self._debug(
"DECtalk USB wrote "
f"{total_written} {description} bytes "
f"to endpoint 0x{endpoint:02x}: {preview}",
debug.DebugLevel.INFO,
on_any_level=True,
)
except (self.usb_core.USBError, OSError) as error:
self._debug(
f"DECtalk USB write failed: {error}",
debug.DebugLevel.ERROR,
on_any_level=True,
)
def _dispose_usb_device(self, usb_device):
try:
self.usb_util.dispose_resources(usb_device)
except Exception as error:
self._debug(
f"DECtalk USB resource disposal failed: {error}",
debug.DebugLevel.WARNING,
)
def _load_usb_modules(self):
import usb.core
import usb.util
return usb.core, usb.util
+273
View File
@@ -0,0 +1,273 @@
import threading
import time
from unittest.mock import Mock
import pytest
from fenrirscreenreader.speechDriver import dectalkUsbDriver
class FakeUsbDevice:
def __init__(self):
self.writes = []
def get_active_configuration(self):
return object()
def write(self, endpoint, data, timeout=None):
payload = bytes(data)
self.writes.append((endpoint, payload, timeout))
return len(payload)
class FakeUsbCore:
USBError = OSError
class NoBackendError(ValueError):
pass
def __init__(self, device, find_error=None):
self.device = device
self.find_error = find_error
self.find_calls = []
def find(self, **kwargs):
self.find_calls.append(kwargs)
if self.find_error:
raise self.find_error
return self.device
class FakeUsbUtil:
def __init__(
self,
claim_error=None,
release_error=None,
dispose_error=None,
):
self.claim_error = claim_error
self.release_error = release_error
self.dispose_error = dispose_error
self.claimed = []
self.released = []
self.disposed = []
def claim_interface(self, device, interface):
if self.claim_error:
raise self.claim_error
self.claimed.append((device, interface))
def release_interface(self, device, interface):
if self.release_error:
raise self.release_error
self.released.append((device, interface))
def dispose_resources(self, device):
if self.dispose_error:
raise self.dispose_error
self.disposed.append(device)
class BlockingUsbDevice(FakeUsbDevice):
def __init__(self):
super().__init__()
self.normal_write_started = threading.Event()
self.release_normal_write = threading.Event()
self.immediate_write_finished = threading.Event()
def write(self, endpoint, data, timeout=None):
if endpoint == 0x02:
self.normal_write_started.set()
self.release_normal_write.wait(timeout=1.0)
result = super().write(endpoint, data, timeout=timeout)
if endpoint == 0x04:
self.immediate_write_finished.set()
return result
def build_environment():
return {
"runtime": {
"SettingsManager": Mock(),
"DebugManager": Mock(),
}
}
def initialized_driver(monkeypatch):
device = FakeUsbDevice()
usb_core = FakeUsbCore(device)
usb_util = FakeUsbUtil()
speech_driver = dectalkUsbDriver.driver()
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, usb_util),
)
speech_driver.initialize(build_environment())
return speech_driver, device, usb_core, usb_util
def wait_for_writes(device, count, timeout=1.0):
deadline = time.monotonic() + timeout
while len(device.writes) < count and time.monotonic() < deadline:
time.sleep(0.01)
def test_dectalk_usb_driver_finds_and_claims_native_device(monkeypatch):
speech_driver, device, usb_core, usb_util = initialized_driver(monkeypatch)
try:
assert usb_core.find_calls == [
{"idVendor": 0x0DD0, "idProduct": 0x2001}
]
assert usb_util.claimed == [(device, 0)]
assert speech_driver._is_initialized
finally:
speech_driver.shutdown()
assert usb_util.released == [(device, 0)]
assert usb_util.disposed == [device]
def test_dectalk_usb_driver_writes_commands_and_speech(monkeypatch):
speech_driver, device, _, _ = initialized_driver(monkeypatch)
try:
speech_driver.set_rate(1.0)
speech_driver.set_pitch(1.0)
speech_driver.set_volume(0.5)
speech_driver.speak("Hello\nworld")
wait_for_writes(device, 4)
assert device.writes == [
(0x02, b"[:ra 650]", 1000),
(0x02, b"[:dv ap 350]", 1000),
(0x02, b"[:vo 50]", 1000),
(0x02, b"Hello world\x0b", 1000),
]
finally:
speech_driver.shutdown()
def test_dectalk_usb_driver_chunks_normal_writes(monkeypatch):
speech_driver, device, _, _ = initialized_driver(monkeypatch)
try:
speech_driver._write_bytes(b"x" * 70, "speech")
assert device.writes == [
(0x02, b"x" * 64, 1000),
(0x02, b"x" * 6, 1000),
]
finally:
speech_driver.shutdown()
def test_dectalk_usb_driver_uses_immediate_cancel_endpoint(monkeypatch):
speech_driver, device, _, _ = initialized_driver(monkeypatch)
try:
speech_driver.cancel()
assert device.writes == [(0x04, b"\x06", 1000)]
finally:
speech_driver.shutdown()
def test_dectalk_usb_cancel_bypasses_blocked_normal_write(monkeypatch):
device = BlockingUsbDevice()
usb_core = FakeUsbCore(device)
usb_util = FakeUsbUtil()
speech_driver = dectalkUsbDriver.driver()
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, usb_util),
)
speech_driver.initialize(build_environment())
normal_thread = threading.Thread(
target=speech_driver._write_bytes,
args=(b"normal speech", "speech"),
)
cancel_thread = threading.Thread(target=speech_driver.cancel)
try:
normal_thread.start()
assert device.normal_write_started.wait(timeout=0.2)
cancel_thread.start()
assert device.immediate_write_finished.wait(timeout=0.2)
finally:
device.release_normal_write.set()
normal_thread.join(timeout=1.0)
cancel_thread.join(timeout=1.0)
speech_driver.shutdown()
def test_dectalk_usb_driver_reports_missing_pyusb(monkeypatch):
speech_driver = dectalkUsbDriver.driver()
def missing_pyusb():
raise ImportError("No module named usb")
monkeypatch.setattr(speech_driver, "_load_usb_modules", missing_pyusb)
with pytest.raises(RuntimeError, match="PyUSB"):
speech_driver.initialize(build_environment())
def test_dectalk_usb_driver_reports_missing_libusb_backend(monkeypatch):
speech_driver = dectalkUsbDriver.driver()
backend_error = FakeUsbCore.NoBackendError("No backend available")
usb_core = FakeUsbCore(None, find_error=backend_error)
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, FakeUsbUtil()),
)
with pytest.raises(RuntimeError, match="discovery failed.*backend"):
speech_driver.initialize(build_environment())
def test_dectalk_usb_driver_reports_missing_device(monkeypatch):
speech_driver = dectalkUsbDriver.driver()
usb_core = FakeUsbCore(None)
usb_util = FakeUsbUtil()
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, usb_util),
)
with pytest.raises(RuntimeError, match="0dd0:2001"):
speech_driver.initialize(build_environment())
def test_dectalk_usb_driver_reports_interface_permission_error(monkeypatch):
speech_driver = dectalkUsbDriver.driver()
device = FakeUsbDevice()
usb_core = FakeUsbCore(device)
usb_util = FakeUsbUtil(claim_error=PermissionError("access denied"))
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, usb_util),
)
with pytest.raises(RuntimeError, match="open failed.*access denied"):
speech_driver.initialize(build_environment())
def test_dectalk_usb_shutdown_survives_cleanup_errors(monkeypatch):
device = FakeUsbDevice()
usb_core = FakeUsbCore(device)
usb_util = FakeUsbUtil(
release_error=OSError("release failed"),
dispose_error=OSError("dispose failed"),
)
speech_driver = dectalkUsbDriver.driver()
monkeypatch.setattr(
speech_driver,
"_load_usb_modules",
lambda: (usb_core, usb_util),
)
speech_driver.initialize(build_environment())
speech_driver.shutdown()
assert not speech_driver._is_initialized
assert speech_driver.usb_device is None
+3
View File
@@ -74,6 +74,9 @@ class TestSpeechSettingsValidation:
self.manager._validate_setting_value("speech", "driver", "speechdDriver") self.manager._validate_setting_value("speech", "driver", "speechdDriver")
self.manager._validate_setting_value("speech", "driver", "genericDriver") self.manager._validate_setting_value("speech", "driver", "genericDriver")
self.manager._validate_setting_value("speech", "driver", "dectalkDriver") self.manager._validate_setting_value("speech", "driver", "dectalkDriver")
self.manager._validate_setting_value(
"speech", "driver", "dectalkUsbDriver"
)
self.manager._validate_setting_value("speech", "driver", "doubletalkDriver") self.manager._validate_setting_value("speech", "driver", "doubletalkDriver")
self.manager._validate_setting_value("speech", "driver", "litetalkDriver") self.manager._validate_setting_value("speech", "driver", "litetalkDriver")
self.manager._validate_setting_value("speech", "driver", "tripletalkDriver") self.manager._validate_setting_value("speech", "driver", "tripletalkDriver")