Files
fenrir/internal/speech/hardware_linux.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
}
}