More tweaks to dectalk driver. Think we're nearly there. Hopefully.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user