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:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+219
View File
@@ -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
}
}