// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later //go:build linux && cgo package speech /* #cgo LDFLAGS: -ldl #include #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 } }