220 lines
5.2 KiB
Go
220 lines
5.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
//go:build linux && cgo
|
|
|
|
package speech
|
|
|
|
/*
|
|
#cgo LDFLAGS: -ldl
|
|
#include <stdlib.h>
|
|
#include "hardware_native.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
type serialPort struct {
|
|
mu sync.Mutex
|
|
native *C.SpeechSerial
|
|
}
|
|
|
|
func open_hardware(id string, options Options) (Transport, error) {
|
|
if err := Validate_driver_options(id, options); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := hardware_parameters(options); err != nil {
|
|
return nil, err
|
|
}
|
|
var port hardwarePort
|
|
if id == "dectalk-usb" {
|
|
var status C.int
|
|
native := C.speech_usb_open(&status)
|
|
if native == nil {
|
|
return nil, usb_error(status)
|
|
}
|
|
usb := &usbPort{
|
|
send: func(endpoint byte, data []byte) error {
|
|
status := C.speech_usb_write(native, C.uchar(endpoint), unsafe.Pointer(&data[0]), C.int(len(data)))
|
|
if status != 0 {
|
|
return usb_error(status)
|
|
}
|
|
return nil
|
|
},
|
|
close: func() { C.speech_usb_close(native) },
|
|
}
|
|
if err := usb.Write([]byte("[:timeout 1]"), func() bool { return false }); err != nil {
|
|
usb.Close()
|
|
return nil, err
|
|
}
|
|
port = usb
|
|
} else {
|
|
path := C.CString(options.Hardware.Device)
|
|
defer C.free(unsafe.Pointer(path))
|
|
baud := options.Hardware.BaudRate
|
|
if baud == 0 {
|
|
baud = 9600
|
|
}
|
|
xon := C.int(0)
|
|
if id == "dectalk" {
|
|
xon = 1
|
|
}
|
|
var status C.int
|
|
native := C.speech_serial_open(path, C.int(baud), xon, &status)
|
|
if native == nil {
|
|
return nil, fmt.Errorf("open hardware speech %s: %w", options.Hardware.Device, syscall.Errno(-status))
|
|
}
|
|
port = &serialPort{native: native}
|
|
}
|
|
return new_hardware_client(id, options, port), nil
|
|
}
|
|
|
|
func (p *serialPort) Write(data []byte, canceled func() bool) error {
|
|
for len(data) > 0 {
|
|
p.mu.Lock()
|
|
if canceled() {
|
|
p.mu.Unlock()
|
|
return errHardwareCanceled
|
|
}
|
|
if p.native == nil {
|
|
p.mu.Unlock()
|
|
return errors.New("serial speech closed")
|
|
}
|
|
count := int(C.speech_serial_write(p.native, unsafe.Pointer(&data[0]), C.size_t(min(len(data), 64))))
|
|
p.mu.Unlock()
|
|
if count > 0 {
|
|
data = data[count:]
|
|
continue
|
|
}
|
|
if count == 0 {
|
|
return io.ErrNoProgress
|
|
}
|
|
err := syscall.Errno(-count)
|
|
if err != syscall.EAGAIN && err != syscall.EINTR {
|
|
return fmt.Errorf("write serial speech: %w", err)
|
|
}
|
|
// XOFF can legitimately last while buffered speech is being spoken.
|
|
// Each write is nonblocking; only this worker waits, and cancellation
|
|
// or shutdown breaks the retry loop without waiting for XON.
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *serialPort) Cancel(command []byte) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.native == nil {
|
|
return nil
|
|
}
|
|
deadline := time.Now().Add(commandTimeout)
|
|
for {
|
|
count := int(C.speech_serial_cancel(p.native, C.uchar(command[0])))
|
|
if count == 1 {
|
|
return nil
|
|
}
|
|
if count == 0 {
|
|
return io.ErrNoProgress
|
|
}
|
|
err := syscall.Errno(-count)
|
|
if (err != syscall.EAGAIN && err != syscall.EINTR) || time.Now().After(deadline) {
|
|
return fmt.Errorf("cancel serial speech: %w", err)
|
|
}
|
|
// Resuming terminal output can complete asynchronously. Retry the
|
|
// immediate byte within a bounded interval, rather than treating the
|
|
// first EAGAIN after TCOON as a device failure.
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (p *serialPort) Close() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.native != nil {
|
|
C.speech_serial_close(p.native)
|
|
p.native = nil
|
|
}
|
|
}
|
|
|
|
// Different endpoints can transfer concurrently. Only Close takes exclusive
|
|
// ownership, after each bounded native call has returned.
|
|
type usbPort struct {
|
|
mu sync.RWMutex
|
|
send func(byte, []byte) error
|
|
close func()
|
|
}
|
|
|
|
func usb_available() error {
|
|
if C.speech_usb_available() == 0 {
|
|
return errors.New("DECtalk USB requires libusb-1.0.so.0")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func usb_error(status C.int) error {
|
|
switch status {
|
|
case -100:
|
|
return errors.New("DECtalk USB requires libusb-1.0.so.0")
|
|
case -101:
|
|
return errors.New("multiple DECtalk USB devices found; connect one for this reader")
|
|
case -102:
|
|
return errors.New("short DECtalk USB write")
|
|
case -4:
|
|
return errors.New("DECtalk USB device 0dd0:2001 unavailable or disconnected")
|
|
case -3:
|
|
return errors.New("DECtalk USB access denied")
|
|
case -6:
|
|
return errors.New("DECtalk USB interface is busy")
|
|
case -7:
|
|
return errors.New("DECtalk USB write timed out")
|
|
}
|
|
return fmt.Errorf("DECtalk USB error %d", status)
|
|
}
|
|
|
|
func (p *usbPort) transfer(endpoint byte, data []byte, canceled func() bool) error {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
if p.send == nil {
|
|
return errors.New("USB speech closed")
|
|
}
|
|
for len(data) > 0 {
|
|
if canceled() {
|
|
return errHardwareCanceled
|
|
}
|
|
count := min(64, len(data))
|
|
if err := p.send(endpoint, data[:count]); err != nil {
|
|
if canceled() {
|
|
return errHardwareCanceled
|
|
}
|
|
return err
|
|
}
|
|
data = data[count:]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *usbPort) Write(data []byte, canceled func() bool) error {
|
|
return p.transfer(0x02, data, canceled)
|
|
}
|
|
func (p *usbPort) Cancel(command []byte) error {
|
|
return p.transfer(0x04, command, func() bool { return false })
|
|
}
|
|
func (p *usbPort) Close() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.send != nil {
|
|
if p.close != nil {
|
|
p.close()
|
|
}
|
|
p.send, p.close = nil, nil
|
|
}
|
|
}
|