More attempts at getting this usb dectalk working.
This commit is contained in:
@@ -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)
|
||||
3. **dectalkDriver** - Serial DECtalk-compatible hardware speech driver
|
||||
- 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
|
||||
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
|
||||
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
|
||||
7. **debugDriver** - Debug speech driver for testing
|
||||
8. **debugDriver** - Debug speech driver for testing
|
||||
- No dependencies
|
||||
|
||||
|
||||
@@ -459,15 +462,22 @@ setting <action> [parameters]
|
||||
- `speech#volume=0.1-1.0` - Speech volume
|
||||
- `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#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_baud_rate=9600` - Hardware synth serial baud rate
|
||||
- `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
|
||||
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 hardware speech.
|
||||
such as `/dev/ttyACM0` or `/dev/ttyUSB0`. Use an explicit
|
||||
`speech#hardware_device` path for serial hardware speech.
|
||||
- `speech#auto_read_incoming=True/False` - Auto-read new text
|
||||
|
||||
*Sound Settings:*
|
||||
|
||||
@@ -36,10 +36,12 @@ progress_monitoring=True
|
||||
enabled=True
|
||||
|
||||
# Select speech driver, options are speechdDriver, genericDriver,
|
||||
# dectalkDriver, litetalkDriver, doubletalkDriver, or tripletalkDriver:
|
||||
# dectalkDriver, dectalkUsbDriver, litetalkDriver, doubletalkDriver,
|
||||
# or tripletalkDriver:
|
||||
driver=speechdDriver
|
||||
#driver=genericDriver
|
||||
#driver=dectalkDriver
|
||||
#driver=dectalkUsbDriver
|
||||
#driver=litetalkDriver
|
||||
#driver=doubletalkDriver
|
||||
#driver=tripletalkDriver
|
||||
@@ -77,6 +79,7 @@ volume=1.0
|
||||
|
||||
# Hardware speech synthesizer serial device.
|
||||
# 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*
|
||||
# or /dev/ttyUSB*. USB-only synths with no tty device need a separate driver.
|
||||
# Set an explicit device for hardware speech.
|
||||
|
||||
@@ -242,6 +242,8 @@ genericDriver - Command-line TTS (espeak, etc.)
|
||||
.IP \[bu] 4
|
||||
dectalkDriver - DECtalk-compatible serial hardware speech
|
||||
.IP \[bu] 4
|
||||
dectalkUsbDriver - Native Access Solutions DECtalk USB hardware speech
|
||||
.IP \[bu] 4
|
||||
litetalkDriver - LiteTalk-compatible serial hardware speech
|
||||
.IP \[bu] 4
|
||||
doubletalkDriver - DoubleTalk LT-compatible serial hardware speech
|
||||
|
||||
+12
-5
@@ -1551,9 +1551,11 @@ enabled=True
|
||||
Values: on=`+True+`, off=`+False+`
|
||||
|
||||
# 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=dectalkDriver
|
||||
#driver=dectalkUsbDriver
|
||||
#driver=litetalkDriver
|
||||
#driver=doubletalkDriver
|
||||
#driver=tripletalkDriver
|
||||
@@ -1684,7 +1686,7 @@ the pico module:
|
||||
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
|
||||
@@ -1700,9 +1702,14 @@ hardware_baud_rate=9600
|
||||
|
||||
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. 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+`,
|
||||
off=`+False+`
|
||||
|
||||
+10
-4
@@ -340,18 +340,24 @@ status formats, which change too frequently to support reliably.
|
||||
- **speechdDriver** - Speech-dispatcher (recommended)
|
||||
- **genericDriver** - Command-line TTS (espeak, etc.)
|
||||
- **dectalkDriver** - Serial DECtalk-compatible hardware speech
|
||||
- **dectalkUsbDriver** - Native Access Solutions DECtalk USB (`0dd0:2001`)
|
||||
- **litetalkDriver** - Serial LiteTalk-compatible hardware speech
|
||||
- **doubletalkDriver** - Serial DoubleTalk LT-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
|
||||
usually appear as `/dev/ttyUSB0`; built-in serial ports may be `/dev/ttyS0`.
|
||||
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
|
||||
separate driver.
|
||||
The `dectalkUsbDriver` autodetects the native Access Solutions DECtalk USB and
|
||||
ignores both serial settings. It requires PyUSB and a libusb backend. From a
|
||||
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
|
||||
- **genericDriver** - Sox-based (default)
|
||||
|
||||
+5
-3
@@ -878,10 +878,11 @@ Turn speech on or off:
|
||||
enabled=True
|
||||
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=genericDriver
|
||||
#driver=dectalkDriver
|
||||
#driver=dectalkUsbDriver
|
||||
#driver=litetalkDriver
|
||||
#driver=doubletalkDriver
|
||||
#driver=tripletalkDriver
|
||||
@@ -894,6 +895,7 @@ Available Drivers:
|
||||
* ''genericDriver'' using the generic driver, for Fenrir <1.5 this is not available
|
||||
* ''speechdDriver'' using speech-dispatcher, for Fenrir <1.5 just use ''speechd''
|
||||
* ''dectalkDriver'' using DECtalk-compatible serial hardware or RPITalk
|
||||
* ''dectalkUsbDriver'' using the native Access Solutions DECtalk USB
|
||||
* ''litetalkDriver'' using LiteTalk-compatible serial hardware or RPITalk
|
||||
* ''doubletalkDriver'' using DoubleTalk LT-compatible serial hardware
|
||||
* ''tripletalkDriver'' using TripleTalk-compatible serial hardware
|
||||
@@ -927,7 +929,7 @@ 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.
|
||||
Serial hardware speech drivers use a serial device. Set an explicit path.
|
||||
hardware_device=/dev/ttyACM0
|
||||
hardware_device=/dev/ttyUSB0
|
||||
hardware_device=/dev/ttyS0
|
||||
@@ -936,7 +938,7 @@ Hardware speech drivers use 9600 baud by default.
|
||||
hardware_baud_rate=9600
|
||||
|
||||
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
|
||||
auto_read_incoming=True
|
||||
|
||||
@@ -106,6 +106,7 @@ setup(
|
||||
"pyte>=0.7.0",
|
||||
],
|
||||
extras_require={
|
||||
"dectalk_usb": ["pyusb>=1.2.1"],
|
||||
"x11": ["python-xlib>=0.33"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -521,6 +521,7 @@ class SettingsManager:
|
||||
"speechdDriver",
|
||||
"genericDriver",
|
||||
"dectalkDriver",
|
||||
"dectalkUsbDriver",
|
||||
"doubletalkDriver",
|
||||
"litetalkDriver",
|
||||
"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
|
||||
@@ -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
|
||||
@@ -74,6 +74,9 @@ class TestSpeechSettingsValidation:
|
||||
self.manager._validate_setting_value("speech", "driver", "speechdDriver")
|
||||
self.manager._validate_setting_value("speech", "driver", "genericDriver")
|
||||
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", "litetalkDriver")
|
||||
self.manager._validate_setting_value("speech", "driver", "tripletalkDriver")
|
||||
|
||||
Reference in New Issue
Block a user