204 lines
7.0 KiB
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;
|
|
}
|