// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later #include "hardware_native.h" #include #include #include #include #include #include #include #include #include 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; }