honeywell-config/honeywell-config.c
s3lph d7223710ea
All checks were successful
/ build_debian (push) Successful in 30s
feat: iterate usb devices and find compatible ones
2024-12-05 03:08:34 +01:00

537 lines
14 KiB
C

#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <libusb-1.0/libusb.h>
#ifndef _VERSION
#define _VERSION "none"
#endif
#define S_ENQ 0x05
#define S_ACK 0x06
#define S_NAK 0x15
#define ISSTATUS(c) (*c == S_ENQ || *c == S_ACK || *c == S_NAK)
#define USBERR(msg, rc) { dprintf(2, "%s: %s\n", msg, libusb_strerror(rc)); }
#define USBDEBUG2(msg, rc) { if (debug >= 3) { dprintf(2, "%s: %s\n", msg, libusb_strerror(rc)); } }
#define DEBUG(...) { if (debug >= 1) { dprintf(2, __VA_ARGS__); } }
#define DEBUG2(...) { if (debug >= 2) { dprintf(2, __VA_ARGS__); } }
#define DEBUG3(...) { if (debug >= 3) { dprintf(2, __VA_ARGS__); } }
#define HEXBUF(msg, buf, n) { if (debug >= 3) { dprintf(2, "%s:", msg); for (uint8_t *__h = buf; __h < buf+n;++__h) { dprintf(2, " %02x", *__h); } dprintf(2, "\n"); } }
uint16_t vendor_id = 0;
uint16_t product_id = 0;
char *serial_number = NULL;
uint8_t reset = 0;
uint8_t oneline = 0;
uint8_t onlyargs = 0;
uint8_t debug = 0;
int infd = 0;
int outfd = 1;
uint8_t explicit_in = 0;
libusb_device_handle *devh = NULL;
uint8_t endpoint = 0;
uint8_t ints[32] = { 0 };
uint8_t inti = 0;
struct tokenizer {
char tokbuf[1024];
char *tok;
uint8_t eot;
uint8_t status;
int (*cb)(const char *, uint8_t , uint8_t);
};
void tok_init(struct tokenizer *t, int (*cb)(const char *, uint8_t, uint8_t)) {
memset(t->tokbuf, 0, 1024);
t->tok = t->tokbuf;
t->eot = 0;
t->status = 0;
t->cb = cb;
}
int tok_read(struct tokenizer *t, char *buf, size_t n) {
char *start = buf;
char *end = NULL, *tmpend = NULL;
while (!t->eot) {
// non-final tokens: terminated by ;
end = strchr(start, ';');
// last token: terminated by .
tmpend = strchr(start, '.');
if (tmpend && (tmpend < end || !end)) {
end = tmpend;
t->eot = 1;
}
if (end) {
if (end > start) {
for (char *c = start; c < end; ++c) {
if (ISSTATUS(c)) {
t->status = *c;
} else {
*(t->tok++) = *c;
}
}
}
*(t->tok) = 0;
if (t->cb(t->tokbuf, t->status, t->eot)) {
return 1;
}
t->tok = t->tokbuf;
*(t->tok) = 0;
start = end + 1;
continue;
}
// incomplete tokens continued in the next message
end = strchr(start, '?');
if (end) {
*end = 0;
}
for (char *c = start; *c != 0; ++c) {
if (ISSTATUS(c)) {
t->status = *c;
} else {
*(t->tok++) = *c;
}
}
*(t->tok) = 0;
break;
}
return 0;
}
struct tokenizer in_tok;
struct tokenizer out_tok;
int print_token(const char *tok, uint8_t status, uint8_t last) {
switch (status) {
case S_ACK:
if (onlyargs && (strlen(tok) <= 6 || !strncmp("REV", tok, 3))) {
return 0;
}
int rc;
if (oneline && !last) {
rc = dprintf(outfd, "%s;", tok);
} else {
rc = dprintf(outfd, "%s.\n", tok);
}
return rc < 0;
case S_ENQ:
dprintf(2, "command not recognized by device: %s\n", tok);
return 1;
case S_NAK:
dprintf(2, "command data out of bounds: %s\n", tok);
return 1;
default:
dprintf(2, "device responded to command %s with unknown status 0x%02x\n", tok, status);
return 1;
}
}
int send_token(const char *tok, uint8_t status, uint8_t last) {
in_tok.eot = 0;
uint8_t cmd[65] = { 0 };
uint8_t *cmdend = cmd + 64;
uint8_t *cmdptr = cmd + 5;
int rc;
memset(cmdptr, 0, 60);
for (uint8_t i = 0; i < strlen(tok) && cmdptr < cmdend; ++i) {
if (isprint(tok[i])) {
*(cmdptr++) = tok[i];
}
}
if (cmdptr >= cmdend) {
dprintf(2, "token too long: %s\n", tok);
return 1;
}
*cmdptr = '.';
cmd[0] = 0xfd;
cmd[1] = cmdptr - cmd - 1;
cmd[2] = 0x16;
cmd[3] = 0x4d;
cmd[4] = 0x0d;
HEXBUF(">", cmd, 64);
rc = libusb_control_transfer(devh, 0x21, 0x09, 0x02fd, 1, cmd, 64, 0);
if (rc < 0) {
USBERR("failed to send control message", rc);
return 1;
} else {
DEBUG2("> %s\n", cmd+5);
}
int tx;
tok_init(&out_tok, print_token);
// Repeat until a "." (EOT) is encountered in the response.
while (!out_tok.eot) {
rc = libusb_interrupt_transfer(devh, endpoint, cmd, 64, &tx, 1000);
if (rc) {
USBERR("failed to receive response from device", rc);
return 1;
}
HEXBUF("<", cmd, tx);
// skip first 5 bytes (report id, length, 3*AIMID)
DEBUG2("< %s\n", cmd+5);
if (tok_read(&out_tok, (char *) cmd+5, tx-5)) {
return 1;
}
}
return 0;
}
int config(int argc, char **args) {
// If commands are provided on the CLI, execute only those
if (argc > 0) {
for (int i = 0; i < argc; ++i) {
if (send_token(args[i], S_ACK, i >= argc-1)) {
return 1;
}
}
return 0;
}
// If input is a tty, dump config
// Can be overwritten with "-i -" on the cli
if (isatty(infd) && !explicit_in) {
return send_token("?", S_ACK, 1);
}
// Otherwhise parse tokens from stdin
char buf[65];
ssize_t n = 1;
tok_init(&in_tok, send_token);
while (n) {
n = read(infd, buf, 64);
if (n < 0) {
perror("failed to read from input file");
return 1;
}
buf[n] = 0;
if (tok_read(&in_tok, buf, n)) {
return 1;
}
}
return 0;
}
int check_device(libusb_device *dev) {
struct libusb_device_descriptor devd = { 0 };
struct libusb_config_descriptor *devc = NULL;
const struct libusb_interface *devi = NULL;
const struct libusb_interface_descriptor *devid = NULL;
const struct libusb_endpoint_descriptor *deved = NULL;
char devdesc[256] = { 0 };
// Top-level checks against VID, PID and Serial
int rc = libusb_get_device_descriptor(dev, &devd);
if (rc) {
USBDEBUG2("failed to get device descriptor", rc);
return rc;
}
if (vendor_id && vendor_id != devd.idVendor) {
return 1;
}
if (product_id && product_id != devd.idProduct) {
return 1;
}
rc = libusb_open(dev, &devh);
if (rc < 0) {
USBDEBUG2("failed to open device", rc);
return rc;
}
if (serial_number) {
rc = libusb_get_string_descriptor_ascii(devh, devd.iSerialNumber, (uint8_t *) devdesc, 256);
if (rc < 0) {
libusb_close(devh);
devh = NULL;
USBDEBUG2("failed to get serial id string descriptor", rc);
return rc;
}
if (strncmp(serial_number, devdesc, strlen(serial_number))) {
libusb_close(devh);
devh = NULL;
return 1;
}
}
// Iterate all configurations, interfaces and endpoints to find the REM/EZConfig endpoint
for (uint8_t c = 0; c < devd.bNumConfigurations; ++c) {
libusb_free_config_descriptor(devc);
rc = libusb_get_config_descriptor(dev, c, &devc);
if (rc) {
USBDEBUG2("failed to get config descriptor", rc);
continue;
}
rc = libusb_get_string_descriptor_ascii(devh, devc->iConfiguration, (uint8_t *) devdesc, 256);
if (rc < 0) {
USBDEBUG2("failed to get config string descriptor", rc);
continue;
}
DEBUG2("- configuration %d: %s\n", c, devdesc);
inti = 0;
for (uint8_t i = 0; i < devc->bNumInterfaces; ++i) {
DEBUG2(" - interface %d\n", i);
devi = &(devc->interface[i]);
for (uint8_t a = 0; a < devi->num_altsetting; ++a) {
devid = &(devi->altsetting[a]);
rc = libusb_get_string_descriptor_ascii(devh, devid->iInterface, (uint8_t *) devdesc, 256);
if (rc < 0) {
USBDEBUG2("failed to get interface string descriptor", rc);
continue;
}
DEBUG2(" - altsetting %d: interface=%d, desc=%s class=%d subclass=%d protocol=%d\n",
devid->bAlternateSetting, devid->bInterfaceNumber, devdesc,
devid->bInterfaceClass, devid->bInterfaceSubClass, devid->bInterfaceProtocol);
for (uint8_t e = 0; e < devid->bNumEndpoints; ++e) {
deved = &(devid->endpoint[e]);
char type = "csbi"[deved->bmAttributes & 0b00000011];
DEBUG2(" - endpoint %x: t=%c\n", deved->bEndpointAddress, type);
// build a list of interfaces so that we can claim all of them
ints[inti++] = devid->bInterfaceNumber;
// endpoint selection:
if ((deved->bmAttributes & 0b11) == LIBUSB_ENDPOINT_TRANSFER_TYPE_INTERRUPT // must be an interrupt endpoint
&& ((deved->bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_IN) // must be a device to host endpoint
&& !strncmp("REM", devdesc, 3) // human readable description of the interface must be "REM" (Honeywell "Remote MasterMind" control interface)
) {
endpoint = deved->bEndpointAddress;
// Print device info
rc = libusb_get_string_descriptor_ascii(devh, devd.iManufacturer, (uint8_t *) devdesc, 256);
if (rc < 0) {
USBDEBUG2("failed to get manufacturer string descriptor", rc);
libusb_close(devh);
devh = NULL;
return 1;
}
DEBUG("Manufacturer: %s\n", devdesc);
rc = libusb_get_string_descriptor_ascii(devh, devd.iProduct, (uint8_t *) devdesc, 256);
if (rc < 0) {
USBDEBUG2("failed to get product string descriptor", rc);
libusb_close(devh);
devh = NULL;
return 1;
}
DEBUG("Product: %s\n", devdesc);
rc = libusb_get_string_descriptor_ascii(devh, devd.iSerialNumber, (uint8_t *) devdesc, 256);
if (rc < 0) {
USBDEBUG2("failed to get serial number string descriptor", rc);
libusb_close(devh);
devh = NULL;
return 1;
}
DEBUG("Serial Number: %s\n", devdesc);
DEBUG2("chose endpoint %x\n", endpoint);
return 0;
}
}
}
}
}
libusb_close(devh);
devh = NULL;
return 1;
}
int usb(int argc, char **args) {
libusb_context *ctx = NULL;
libusb_device **list = NULL;
int rc = 0, fail = 0;
//
// Find and open the device
//
rc = libusb_init(&ctx);
if (!ctx) {
USBERR("failed to initialize libusb", rc);
fail = 1;
goto fail;
}
//
// Iterate list of USB devices
//
ssize_t ndev = libusb_get_device_list(ctx, &list);
if (ndev < 0) {
USBERR("failed to list usb devices", ndev);
fail = 1;
goto fail;
}
for (size_t i = 0; i < ndev; ++i) {
rc = check_device(list[i]);
if (!rc) {
break;
}
}
if (!devh) {
dprintf(2, "failed to find compatible device\n");
fail = 1;
goto fail;
}
//
// Disconnect usbhid driver from both device interfaces
//
rc = libusb_set_auto_detach_kernel_driver(devh, 1);
if (rc) {
USBERR("auto-attaching kernel driver not possible; please replug USB when done", rc);
for (uint8_t i = 0; i < inti; ++i) {
rc = libusb_kernel_driver_active(devh, ints[i]);
if (rc < 0) {
USBERR("failed to query kernel driver state", rc);
fail = 1;
goto fail;
}
if (rc > 0) {
rc = libusb_detach_kernel_driver(devh, ints[i]);
if (rc) {
USBERR("failed to detach kernel driver", rc);
fail = 1;
goto fail;
}
}
}
}
for (uint8_t i = 0; i < inti; ++i) {
rc = libusb_claim_interface(devh, ints[i]);
if (rc) {
USBERR("failed to claim interface", rc);
fail = 1;
goto fail;
}
}
//
// Prepare and send a message on the control channel for each config argument
//
rc = config(argc, args);
if (rc) {
fail = 1;
goto fail;
}
//
// Cleanup
//
fail:
if (devh) {
for (uint8_t i = 0; i < inti; ++i) {
rc = libusb_release_interface(devh, ints[i]);
if (rc) {
USBERR("failed to release interface", rc);
}
}
if (reset) {
libusb_reset_device(devh);
}
libusb_close(devh);
}
if (list) {
libusb_free_device_list(list, 1);
}
if (ctx) {
libusb_exit(ctx);
}
return fail;
}
int main(int argc, char **argv) {
int opt;
char *end;
long parsed;
while ((opt = getopt(argc, argv, "v:p:s:1crdi:o:hV")) != -1) {
switch (opt) {
case 'v':
end = optarg;
parsed = strtol(optarg, &end, 16);
if (*end != 0) {
perror("failed to parse vendor id");
return 1;
}
if (parsed <= 0 || parsed > 0xffff) {
puts("invalid vendor id");
return 1;
}
vendor_id = (uint16_t) parsed;
break;
case 'p':
end = optarg;
parsed = strtol(optarg, &end, 16);
if (*end != 0) {
perror("failed to parse product id");
return 1;
}
if (parsed <= 0 || parsed > 0xffff) {
puts("invalid product id");
return 1;
}
product_id = (uint16_t) parsed;
break;
case 's':
serial_number = strdup(optarg);
break;
case 'r':
reset = 1;
break;
case '1':
oneline = 1;
break;
case 'c':
onlyargs = 1;
break;
case 'd':
++debug;
break;
case 'i':
explicit_in = 1;
if (!strcmp("-", optarg)) {
infd = 0;
} else {
infd = open(optarg, O_CLOEXEC|O_RDONLY);
if (infd < 0) {
perror("failed to open input file");
exit(1);
}
}
break;
case 'o':
if (!strcmp("-", optarg)) {
outfd = 1;
} else {
outfd = open(optarg, O_CLOEXEC|O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
if (outfd < 0) {
perror("failed to open output file");
exit(1);
}
}
break;
case 'V':
puts(_VERSION);
exit(0);
break;
default:
printf("%s [-v vendor] [-p product] [-1] [-c] [-r] [-d] [-i infile] [-o outfile] [command1 [... commandN]]\n", argv[0]);
puts(" -v vendor\tUSB vendor ID of the barcode scanner to configure.");
puts(" -p product\tUSB product ID of the barcode scanner to configure.");
puts(" -s serial\tUSB serial number of the barcode scanner to configure.");
puts(" -1\t\tPrint response on one line, rather than one token per line.");
puts(" -c\t\tPrint only received commands that contain an argument. Useful for config backups.");
puts(" -r\t\tReset device after configuration.");
puts(" -d\t\tDebug mode (more verbose output). Can be provided repeatedly to increase verbosity.");
puts(" -i infile\tRead input from infile instead of stdin.");
puts(" -o outfile\tWrite output to outfile instead of stdout.");
puts(" -h \t\tShow this help and exit.");
puts(" -V \t\tShow the version number and exit.");
return 1;
}
}
return usb(argc-optind, argv+optind);
}