More tweaks to dectalk driver. Think we're nearly there. Hopefully.

This commit is contained in:
Storm Dragon
2026-07-26 21:24:28 -04:00
parent e93aba109a
commit fac3065c6a
4 changed files with 139 additions and 3 deletions
@@ -14,7 +14,11 @@ class driver(hardware_serial_driver):
use_xon_xoff = True
def _speak_bytes(self, text):
return self._clean_text(text).encode("ascii", errors="replace") + b"\x0b"
text = self._neutralize_inline_commands(self._clean_text(text))
return text.encode("ascii", errors="replace") + b"\x0b"
def _neutralize_inline_commands(self, text):
return text.replace("[:", "[ :")
def _rate_command(self, rate):
value = self._scale_around_default(rate, 75, 180, 650)
@@ -19,6 +19,11 @@ class driver(dectalkDriver.driver):
packet_size = 64
write_timeout = 1000
def __init__(self):
super().__init__()
self._speech_cancel_generation = 0
self._speech_cancel_lock = threading.Lock()
def initialize(self, environment):
self.env = environment
self._is_initialized = False
@@ -129,6 +134,7 @@ class driver(dectalkDriver.driver):
def cancel(self):
if not self._is_initialized:
return
self._advance_speech_cancel_generation()
self.clear_buffer()
self._write_endpoint(
self.immediate_endpoint,
@@ -137,8 +143,56 @@ class driver(dectalkDriver.driver):
write_lock=self.immediate_lock,
)
def _write_bytes(self, data, description="data"):
self._write_endpoint(self.speech_endpoint, data, description)
def speak(self, text, queueable=True, ignore_punctuation=False):
if not self._is_initialized:
return
if not queueable:
self.cancel()
if not isinstance(text, str) or text == "":
return
self._debug(
"Hardware speech queued text: "
f"{len(text)} chars, queue_size={self.text_queue.qsize()}",
debug.DebugLevel.INFO,
on_any_level=True,
)
self.text_queue.put(
(self._get_speech_cancel_generation(), text)
)
def _worker(self):
while not self._stop_worker:
queue_item = self.text_queue.get()
if queue_item is None:
return
cancel_generation, text = queue_item
if cancel_generation != self._get_speech_cancel_generation():
continue
try:
data = self._speak_bytes(text)
self._debug(
"Hardware speech worker prepared speech bytes: "
f"{len(data)} bytes",
debug.DebugLevel.INFO,
on_any_level=True,
)
self._write_bytes(data, "speech", cancel_generation)
except Exception as error:
self._debug(
f"Hardware speech worker failed: {error}",
debug.DebugLevel.ERROR,
on_any_level=True,
)
def _write_bytes(self, data, description="data", cancel_generation=None):
if description == "speech" and cancel_generation is None:
cancel_generation = self._get_speech_cancel_generation()
self._write_endpoint(
self.speech_endpoint,
data,
description,
cancel_generation=cancel_generation,
)
def _write_endpoint(
self,
@@ -146,6 +200,7 @@ class driver(dectalkDriver.driver):
data,
description,
write_lock=None,
cancel_generation=None,
):
if not data:
return
@@ -157,6 +212,15 @@ class driver(dectalkDriver.driver):
try:
total_written = 0
while total_written < len(data):
if self._speech_cancel_requested(cancel_generation):
self._debug(
"DECtalk USB stopped speech write after "
f"{total_written} bytes because speech was "
"cancelled",
debug.DebugLevel.INFO,
on_any_level=True,
)
return
chunk = data[
total_written : total_written + self.packet_size
]
@@ -186,6 +250,19 @@ class driver(dectalkDriver.driver):
on_any_level=True,
)
def _advance_speech_cancel_generation(self):
with self._speech_cancel_lock:
self._speech_cancel_generation += 1
def _get_speech_cancel_generation(self):
with self._speech_cancel_lock:
return self._speech_cancel_generation
def _speech_cancel_requested(self, cancel_generation):
if cancel_generation is None:
return False
return cancel_generation != self._get_speech_cancel_generation()
def _dispose_usb_device(self, usb_device):
try:
self.usb_util.dispose_resources(usb_device)
+43
View File
@@ -85,6 +85,19 @@ class BlockingUsbDevice(FakeUsbDevice):
return result
class CancelDuringSpeechUsbDevice(FakeUsbDevice):
def __init__(self):
super().__init__()
self.first_normal_write_started = threading.Event()
self.release_first_normal_write = threading.Event()
def write(self, endpoint, data, timeout=None):
if endpoint == 0x02 and not self.first_normal_write_started.is_set():
self.first_normal_write_started.set()
self.release_first_normal_write.wait(timeout=1.0)
return super().write(endpoint, data, timeout=timeout)
def build_environment():
return {
"runtime": {
@@ -197,6 +210,36 @@ def test_dectalk_usb_cancel_bypasses_blocked_normal_write(monkeypatch):
speech_driver.shutdown()
def test_dectalk_usb_cancel_stops_current_speech_upload(monkeypatch):
device = CancelDuringSpeechUsbDevice()
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"x" * 192, "speech"),
)
try:
normal_thread.start()
assert device.first_normal_write_started.wait(timeout=0.2)
speech_driver.cancel()
finally:
device.release_first_normal_write.set()
normal_thread.join(timeout=1.0)
speech_driver.shutdown()
normal_writes = [
payload for endpoint, payload, _ in device.writes if endpoint == 0x02
]
assert normal_writes == [b"x" * 64]
def test_dectalk_usb_driver_reports_missing_pyusb(monkeypatch):
speech_driver = dectalkUsbDriver.driver()
@@ -70,6 +70,18 @@ def test_dectalk_driver_speaks_printable_text(serial_pair):
speech_driver.shutdown()
def test_dectalk_driver_neutralizes_inline_commands_in_speech(
serial_pair,
):
speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair)
try:
speech_driver.speak("status [:dv g5 10] done")
expected = b"status [ :dv g5 10] done\x0b"
assert read_available(master_fd, len(expected)) == expected
finally:
speech_driver.shutdown()
def test_dectalk_driver_writes_settings_and_cancel(serial_pair):
speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair)
try: