After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.
This commit is contained in:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -0,0 +1,219 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user