Files
fenrir/internal/speech/hardware_native.c

204 lines
7.0 KiB
C

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
#include "hardware_native.h"
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
struct SpeechSerial {
int fd;
struct termios original;
};
SpeechSerial *speech_serial_open(const char *path, int baud, int xon, int *status) {
SpeechSerial *port = calloc(1, sizeof(*port));
*status = -ENOMEM;
if (!port) return NULL;
port->fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
if (port->fd < 0) goto failed;
// A short-lived candidate must never configure another reader's device.
if (flock(port->fd, LOCK_EX | LOCK_NB) < 0) goto failed;
if (tcgetattr(port->fd, &port->original) < 0) goto failed;
struct termios options = port->original;
cfmakeraw(&options);
options.c_cflag |= CLOCAL | CREAD;
options.c_cflag &= ~(CRTSCTS | CSTOPB);
options.c_iflag &= ~(IXON | IXOFF | IXANY);
if (xon) options.c_iflag |= IXON;
options.c_cc[VSTART] = 0x11;
options.c_cc[VSTOP] = 0x13;
speed_t speed;
switch (baud) {
case 1200: speed = B1200; break;
case 2400: speed = B2400; break;
case 4800: speed = B4800; break;
case 9600: speed = B9600; break;
case 19200: speed = B19200; break;
case 38400: speed = B38400; break;
case 57600: speed = B57600; break;
case 115200: speed = B115200; break;
default: errno = EINVAL; goto failed;
}
cfsetispeed(&options, speed);
cfsetospeed(&options, speed);
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 0;
if (tcsetattr(port->fd, TCSANOW, &options) < 0) goto failed;
*status = 0;
return port;
failed:
*status = -errno;
if (port->fd >= 0) close(port->fd);
free(port);
return NULL;
}
int speech_serial_write(SpeechSerial *port, const void *data, size_t length) {
ssize_t result = write(port->fd, data, length);
return result < 0 ? -errno : (int)result;
}
int speech_serial_cancel(SpeechSerial *port, unsigned char command) {
if (tcflush(port->fd, TCOFLUSH) < 0) return -errno;
if (tcflow(port->fd, TCOON) < 0) return -errno;
// TCOON clears an explicit tcflow stop, but not a received XOFF in Linux's
// N_TTY discipline. Temporarily clearing IXON restarts that output path.
// Restore software flow control immediately after submitting the cancel.
struct termios options;
if (tcgetattr(port->fd, &options) < 0) return -errno;
int xon = (options.c_iflag & IXON) != 0;
if (xon) {
struct termios resumed = options;
resumed.c_iflag &= ~IXON;
if (tcsetattr(port->fd, TCSANOW, &resumed) < 0) return -errno;
}
int result = speech_serial_write(port, &command, 1);
if (xon && tcsetattr(port->fd, TCSANOW, &options) < 0) return -errno;
return result;
}
void speech_serial_close(SpeechSerial *port) {
tcsetattr(port->fd, TCSANOW, &port->original);
close(port->fd);
free(port);
}
// Optional libusb 1.0 ABI, resolved at runtime like Enchant. Neither headers
// nor a link-time libusb dependency are required for other speech drivers.
typedef struct libusb_context USBContext;
typedef struct libusb_device USBDevice;
typedef struct libusb_device_handle USBHandle;
struct USBDescriptor {
uint8_t length, type;
uint16_t usb;
uint8_t device_class, subclass, protocol, packet_size;
uint16_t vendor, product, device;
uint8_t manufacturer, product_name, serial, configurations;
};
struct SpeechUSB {
void *library;
USBContext *context;
USBHandle *handle;
int claimed;
int (*init)(USBContext **);
void (*exit)(USBContext *);
ssize_t (*get_device_list)(USBContext *, USBDevice ***);
void (*free_device_list)(USBDevice **, int);
int (*get_device_descriptor)(USBDevice *, struct USBDescriptor *);
int (*open)(USBDevice *, USBHandle **);
void (*close)(USBHandle *);
int (*get_configuration)(USBHandle *, int *);
int (*set_configuration)(USBHandle *, int);
int (*claim_interface)(USBHandle *, int);
int (*release_interface)(USBHandle *, int);
int (*bulk_transfer)(USBHandle *, unsigned char, unsigned char *, int, int *, unsigned int);
};
void speech_usb_close(SpeechUSB *port) {
if (!port) return;
if (port->claimed) port->release_interface(port->handle, 0);
if (port->handle) port->close(port->handle);
if (port->context) port->exit(port->context);
if (port->library) dlclose(port->library);
free(port);
}
static SpeechUSB *load_usb(void) {
SpeechUSB *port = calloc(1, sizeof(*port));
if (!port) return NULL;
port->library = dlopen("libusb-1.0.so.0", RTLD_NOW | RTLD_LOCAL);
if (!port->library) goto failed;
#define LOAD(field) port->field = dlsym(port->library, "libusb_" #field); if (!port->field) goto failed
LOAD(init); LOAD(exit); LOAD(get_device_list); LOAD(free_device_list);
LOAD(get_device_descriptor); LOAD(open); LOAD(close);
LOAD(get_configuration); LOAD(set_configuration);
LOAD(claim_interface); LOAD(release_interface); LOAD(bulk_transfer);
#undef LOAD
return port;
failed:
speech_usb_close(port);
return NULL;
}
int speech_usb_available(void) {
SpeechUSB *port = load_usb();
if (!port) return 0;
speech_usb_close(port);
return 1;
}
SpeechUSB *speech_usb_open(int *status) {
*status = -100;
SpeechUSB *port = load_usb();
if (!port) return NULL;
*status = port->init(&port->context);
if (*status < 0) goto failed;
USBDevice **devices = NULL;
ssize_t count = port->get_device_list(port->context, &devices);
if (count < 0) { *status = (int)count; goto failed; }
USBDevice *selected = NULL;
int matches = 0;
for (ssize_t index = 0; index < count; index++) {
struct USBDescriptor descriptor;
if (port->get_device_descriptor(devices[index], &descriptor) == 0 &&
descriptor.vendor == 0x0dd0 && descriptor.product == 0x2001) {
selected = devices[index];
matches++;
}
}
*status = matches == 0 ? -4 : -101;
if (matches == 1) *status = port->open(selected, &port->handle);
port->free_device_list(devices, 1);
if (*status < 0) goto failed;
int configuration = 0;
*status = port->get_configuration(port->handle, &configuration);
if (*status < 0) goto failed;
if (configuration == 0) {
*status = port->set_configuration(port->handle, 1);
if (*status < 0) goto failed;
}
*status = port->claim_interface(port->handle, 0);
if (*status < 0) goto failed;
port->claimed = 1;
return port;
failed:
speech_usb_close(port);
return NULL;
}
int speech_usb_write(SpeechUSB *port, unsigned char endpoint, void *data, int length) {
int transferred = 0;
int result = port->bulk_transfer(port->handle, endpoint, data, length, &transferred, 1000);
// Never replay a partial failed transfer: it may already be on the wire.
if (result < 0) return result;
return transferred == length ? 0 : -102;
}