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
|
use_xon_xoff = True
|
||||||
|
|
||||||
def _speak_bytes(self, text):
|
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):
|
def _rate_command(self, rate):
|
||||||
value = self._scale_around_default(rate, 75, 180, 650)
|
value = self._scale_around_default(rate, 75, 180, 650)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class driver(dectalkDriver.driver):
|
|||||||
packet_size = 64
|
packet_size = 64
|
||||||
write_timeout = 1000
|
write_timeout = 1000
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._speech_cancel_generation = 0
|
||||||
|
self._speech_cancel_lock = threading.Lock()
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
self._is_initialized = False
|
self._is_initialized = False
|
||||||
@@ -129,6 +134,7 @@ class driver(dectalkDriver.driver):
|
|||||||
def cancel(self):
|
def cancel(self):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
|
self._advance_speech_cancel_generation()
|
||||||
self.clear_buffer()
|
self.clear_buffer()
|
||||||
self._write_endpoint(
|
self._write_endpoint(
|
||||||
self.immediate_endpoint,
|
self.immediate_endpoint,
|
||||||
@@ -137,8 +143,56 @@ class driver(dectalkDriver.driver):
|
|||||||
write_lock=self.immediate_lock,
|
write_lock=self.immediate_lock,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _write_bytes(self, data, description="data"):
|
def speak(self, text, queueable=True, ignore_punctuation=False):
|
||||||
self._write_endpoint(self.speech_endpoint, data, description)
|
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(
|
def _write_endpoint(
|
||||||
self,
|
self,
|
||||||
@@ -146,6 +200,7 @@ class driver(dectalkDriver.driver):
|
|||||||
data,
|
data,
|
||||||
description,
|
description,
|
||||||
write_lock=None,
|
write_lock=None,
|
||||||
|
cancel_generation=None,
|
||||||
):
|
):
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
@@ -157,6 +212,15 @@ class driver(dectalkDriver.driver):
|
|||||||
try:
|
try:
|
||||||
total_written = 0
|
total_written = 0
|
||||||
while total_written < len(data):
|
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[
|
chunk = data[
|
||||||
total_written : total_written + self.packet_size
|
total_written : total_written + self.packet_size
|
||||||
]
|
]
|
||||||
@@ -186,6 +250,19 @@ class driver(dectalkDriver.driver):
|
|||||||
on_any_level=True,
|
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):
|
def _dispose_usb_device(self, usb_device):
|
||||||
try:
|
try:
|
||||||
self.usb_util.dispose_resources(usb_device)
|
self.usb_util.dispose_resources(usb_device)
|
||||||
|
|||||||
@@ -85,6 +85,19 @@ class BlockingUsbDevice(FakeUsbDevice):
|
|||||||
return result
|
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():
|
def build_environment():
|
||||||
return {
|
return {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
@@ -197,6 +210,36 @@ def test_dectalk_usb_cancel_bypasses_blocked_normal_write(monkeypatch):
|
|||||||
speech_driver.shutdown()
|
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):
|
def test_dectalk_usb_driver_reports_missing_pyusb(monkeypatch):
|
||||||
speech_driver = dectalkUsbDriver.driver()
|
speech_driver = dectalkUsbDriver.driver()
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ def test_dectalk_driver_speaks_printable_text(serial_pair):
|
|||||||
speech_driver.shutdown()
|
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):
|
def test_dectalk_driver_writes_settings_and_cancel(serial_pair):
|
||||||
speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair)
|
speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user