diff --git a/Makefile b/Makefile index 10bcab8..c23de7a 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,12 @@ -CFLAGS=-g -Wall `pkg-config --cflags hidapi` -LDFLAGS=`pkg-config --libs hidapi` +CFLAGS=-c -D_LINUX -g -Wall -I/usr/include/hidapi -I/usr/include/libusb-1.0 +LDFLAGS=-L/usr/lib/x86_64-linux-gnu/ -lhidapi-hidraw -lusb-1.0 all: lsusb howard lsusb: lsusb.c - gcc -olsusb $(CFLAGS) $(LDFLAGS) lsusb.c + gcc $(CFLAGS) lsusb.c -olsusb.o + gcc lsusb.o $(LDFLAGS) -o lsusb howard: howard.c howard.h - gcc -ohoward $(CFLAGS) $(LDFLAGS) howard.c + gcc $(CFLAGS) howard.c -ohoward.o + gcc howard.o $(LDFLAGS) -o howard diff --git a/README.md b/README.md index 44e5955..d901e02 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Overview - Sound level monitoring via USB Benetech GM1356. Named after [Loud Howard](https://dilbert.com/strip/1995-04-21). -Requirements: +Big thanks to [Maciej Ciemborowicz](https://github.com/ciembor) for his work documenting the [USB protocol](https://github.com/dobra-noc/gm1356/blob/master/PROTOCOL.md) this device uses. + +# Requirements - [hidapi](https://github.com/libusb/hidapi) - An OS which recognises the GM1356 as a USB device (ie. not OSX Big Sur). @@ -17,4 +18,6 @@ TBC TBC +# Implementation Notes +- The device seems to 'hang' periodically and cause all subsequent reads to timeout. To work around this, after two concurrent timeouts I send an `IOCTL_USBDEVFS_RESET`. diff --git a/TODO b/TODO new file mode 100644 index 0000000..5715d86 --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +usage() +tidy up output +warn() etc to stderr +code tidy diff --git a/howard.c b/howard.c index 396c10a..e7be98a 100644 --- a/howard.c +++ b/howard.c @@ -1,6 +1,11 @@ #include #include +#ifdef _LINUX +#include +#include +#endif + #ifdef _TERMUX #include #include @@ -14,10 +19,16 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include +#include +#include #include "howard.h" #define TRUE (-1) @@ -29,11 +40,12 @@ // Benestar sound meter //#define MIC_VID (0x1630) //#define MIC_PID (0x05dc) -#define MIC_VID (0x8817) -#define MIC_PID (0x2109) +#define MIC_VID "64bd" +#define MIC_PID "74e3" #define CMD_CAPTURE (0xb3) +hid_device *dev = NULL; const char *rangestr[] = { "30-130", @@ -43,10 +55,18 @@ const char *rangestr[] = { "80-130", }; -char influxhost[BUFLENSMALL]; -char influxdb[BUFLENSMALL]; -char influxuser[BUFLENSMALL]; -char influxpass[BUFLENSMALL]; +char *influxhost = NULL; +char *influxdb = NULL; +char *influxuser = NULL; +char *influxpass = NULL; +int doit = FALSE; +int verbose = FALSE; + +int outmode = 0; + +#define OM_SHORT (1) +#define OM_LONG (2) +#define OM_DB (4) #define FLAG_DBCMODE (0x10) #define FLAG_FASTMODE (0x40) @@ -74,6 +94,67 @@ void colprintf( char *prefix, const char *col, char *format, va_list *args ) { printf("%s\n",PLAIN); } +char *getdevpath(int vid, int pid, char *retvar) { + struct libusb_device **devlist; + struct libusb_context *ctx; + int i,ndevices,found = 0; + + if (libusb_init(&ctx)) { + err("libusb_init() failed"); + return NULL; + } + + ndevices = libusb_get_device_list(ctx, &devlist); + for (i = 0; i < ndevices; i++) { + struct libusb_device* thisdev = devlist[i]; + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(thisdev, &desc); + if (desc.idVendor == vid && desc.idProduct == pid) { + int busnum = -1,devnum = -1; + busnum = libusb_get_bus_number(thisdev); + devnum = libusb_get_device_address(thisdev); + snprintf(retvar, BUFLEN, "/dev/bus/usb/%03d/%03d",busnum,devnum); + found = 1; + break; + } + } + + libusb_free_device_list(devlist, 1); + libusb_exit(ctx); + + if (found) { + return retvar; + } + return NULL; +} + +int resetusb(char *filename) { + int fd,rv = 0; + strncpy(filename, "/dev/bus/usb/003/006", BUFLEN); + + warn("Attempting to reset usb port"); + fd = open(filename, O_WRONLY); + if (fd < 0) { + err("Failed to open %s, details:", filename); + perror("open()"); + rv = 1; + } else { + int rc; + rc = ioctl(fd, USBDEVFS_RESET, 0); + if (rc < 0) { + err("Failed to send reset, details:", filename); + perror("ioctl()"); + rv = 1; + } else { + info("Reset successful"); + rv = 0; + } + close(fd); + } + return rv; +} + + void info( char* format, ... ) { va_list args; va_start(args, format); @@ -81,6 +162,14 @@ void info( char* format, ... ) { va_end( args ); } +void vinfo( char* format, ... ) { + va_list args; + if (!verbose) return; + va_start(args, format); + colprintf( "*", CYAN, format, &args); + va_end( args ); +} + void err( char* format, ... ) { va_list args; va_start(args, format); @@ -107,19 +196,29 @@ int readresult(hid_device *dev, uint8_t *retbuf) { int totbytes = 0; int wantbytes = 8; time_t starttime; - int timeoutms = 2000; + double timeoutsecs = 0.5; + double timeoutms = timeoutsecs*1000; starttime = time(NULL); while (totbytes != wantbytes) { int thisbytes = 0; thisbytes = hid_read_timeout(dev, &retbuf[totbytes], wantbytes + 1 - totbytes, timeoutms); if (thisbytes < 0) { - err("Incomplete read from usb (got %d bytes, want %d bytes): %ls",totbytes,wantbytes, hid_error(dev)); + warn("Incomplete read from usb (got %d bytes, want %d bytes): %ls",totbytes,wantbytes, hid_error(dev)); return E_BADREAD; } totbytes += thisbytes; - if (time(NULL) - starttime > (timeoutms*1000)) { + if (thisbytes > 0) { + vinfo("Got %d/%d bytes",totbytes,wantbytes); + } + + if (thisbytes > wantbytes) { + warn("Bad read from usb (got %d bytes, want %d bytes): %ls",totbytes,wantbytes, hid_error(dev)); + return E_BADREAD; + } + + if (time(NULL) - starttime > timeoutsecs) { warn("Timeout reading from usb"); return E_TIMEOUT; } @@ -127,34 +226,53 @@ int readresult(hid_device *dev, uint8_t *retbuf) { return 0; } -void showlevel(uint8_t *buf) { - uint16_t decibels; +void dooutput(uint8_t *buf) { + double decibels; uint8_t flags, rangeidx; time_t now; char *longformat = "%12s: %ld\n"; char *strformat = "%12s: %s\n"; char *dbformat = "%12s: %4.2f %s\n"; - decibels = buf[0] << 8 | buf[1]; + char range[BUFLENSMALL]; + char dbunits[BUFLENSMALL]; + char checkmode[BUFLENSMALL]; + decibels = getdecibels(buf); flags = buf[2]; rangeidx = buf[2] & 0xf; now = time(NULL); + strncpy(dbunits, flags & FLAG_DBCMODE ? "dBC" : "dBA", BUFLENSMALL); + strncpy(checkmode, flags & FLAG_FASTMODE ? "Fast" : "Slow", BUFLENSMALL); + strncpy(range, rangeidx > 0x4 ? "unknown" : rangestr[rangeidx], BUFLENSMALL); - printf(longformat, "Time", now); - printf(dbformat, "Level", (double)decibels/10.0, flags & FLAG_DBCMODE ? "dBC" : "dBA"); - printf(strformat, "Mode", flags & FLAG_FASTMODE ? "Fast" : "Slow"); - printf(strformat, "Range", rangeidx > 0x4 ? "unknown" : rangestr[rangeidx]); + if (outmode & OM_LONG) { + printf(longformat, "Time", now); + printf(dbformat, "Level", decibels, dbunits); + printf(strformat, "Mode", checkmode); + printf(strformat, "Range", range); + } + if (outmode & OM_SHORT) { + printf("time=[%ld] level=[%4.2f %s] mode=[%s] range=[%s]\n", + now, decibels, dbunits, checkmode, range); + } + if (outmode & OM_DB) { + influx_write_decibels(decibels); + } } int tcpconnect(char *hname, int port) { int sockfd; struct sockaddr_in servaddr; struct hostent *hptr; + fd_set fdset; + struct timeval tv; + int flags; hptr = resolve(hname); - if (!hptr) return TRUE; + if (!hptr) return -1; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { +err("socket"); perror("socket()"); return -1; } @@ -162,11 +280,34 @@ int tcpconnect(char *hname, int port) { servaddr.sin_port = htons(8086); memcpy(&servaddr.sin_addr, hptr->h_addr_list[0], hptr->h_length); bzero(servaddr.sin_zero, sizeof(servaddr.sin_zero)); + + fcntl(sockfd, F_SETFL, O_NONBLOCK); + if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr))) { - printf("errno is %d\n", errno); - perror("tcpconnect()"); - return -1; + if (errno == EINPROGRESS) { + FD_ZERO(&fdset); + FD_SET(sockfd, &fdset); + tv.tv_sec = 2; + tv.tv_usec = 0; + if (select(sockfd+1, NULL, &fdset, NULL, &tv) == 1) { + int so_error; + socklen_t len = sizeof so_error; + getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len); + if (so_error != 0) { + perror("getsockopt(d)"); + close(sockfd); + return -1; + } + } + } else { + perror("tcpconnect()"); + return -1; + } } + + flags = fcntl(sockfd, F_GETFL); + fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK); + return sockfd; } @@ -197,7 +338,8 @@ int dohttp(char *hname, int port, char *header, char *body, char *retbuf) { // get response n = read(sockfd, response, sizeof(response)); if (n < 0) { - err("POST failed during response read"); + err("POST failed during response read (n=%d)", n); + perror("read()"); close(sockfd); return -1; } @@ -244,11 +386,29 @@ struct hostent *resolve(char *hname) { return hptr; } +char *append(char *orig, char *new, int maxlen) { + if (!orig) return NULL; + if (!new) return orig; + if (strlen(orig)) { + strncat(orig, ", ", maxlen); + strncat(orig, new, maxlen); + } else { + strncpy(orig, new, maxlen); + } + return orig; +} + int influx_init(char *hname, char *db, char *user, char *pass) { - strncpy(influxhost, hname, BUFLENSMALL); - strncpy(influxdb, db, BUFLENSMALL); - strncpy(influxuser, user, BUFLENSMALL); - strncpy(influxpass, pass, BUFLENSMALL); + char missing[BUFLEN]; + strncpy(missing, "", BUFLEN); + if (!hname) append(missing, "hostname (-H)", BUFLEN); + if (!db) append(missing, "dbname (-d)", BUFLEN); + if (!user) append(missing, "username (-u)", BUFLEN); + if (!pass) append(missing, "password (-p)", BUFLEN); + if (strlen(missing)) { + err("missing influxdb details: %s", missing); + return TRUE; + } return FALSE; } @@ -258,7 +418,7 @@ int influx_cmd(enum influxcmdtype cmdtype, char *cmd, char *retbuf) { int rc = -1; if (cmdtype == I_WRITE) { - snprintf(header, BUFLEN, "POST /write?db=%s&u=%s&p=%s HTTP/1.1\r\nHost: influx:8086\r\nContent-Length: %ld\r\n\r\n", influxdb, influxuser, influxpass, strlen(cmd)); + snprintf(header, BUFLEN, "POST /write?db=%s&u=%s&p=%s&precision=s HTTP/1.1\r\nHost: influx:8086\r\nContent-Length: %ld\r\n\r\n", influxdb, influxuser, influxpass, strlen(cmd)); snprintf(newcmd,BUFLEN,"%s",cmd); } else if (cmdtype == I_READ) { snprintf(header, BUFLEN, "GET /query?db=%s&u=%s&p=%s HTTP/1.1\r\nHost: influx:8086\r\nContent-Length: %ld\r\n\r\n", influxdb, influxuser, influxpass, strlen(cmd)+2); @@ -268,12 +428,12 @@ int influx_cmd(enum influxcmdtype cmdtype, char *cmd, char *retbuf) { strncpy(newcmd,"",BUFLEN); } else { err("Invalid influx command type (%d)", cmdtype); - return TRUE; + return FALSE; } rc = dohttp(influxhost, 8086, header, newcmd, retbuf); if (rc != 204) { - err("Influx write to %s/%s failed (HTTP %d)",influxhost,influxdb,rc); + err("Influx write to %s:8086 db %s failed (HTTP %d)",influxhost,influxdb,rc); return TRUE; } return FALSE; @@ -291,16 +451,42 @@ int influx_ping(char *retbuf) { int rv; rv = influx_cmd(I_PING, "", retbuf); if (!rv) { - info("Ping success:\n%s\n", retbuf); + vinfo("Ping success"); } return rv; } int influx_write_decibels(double decibels) { char cmd[BUFLEN]; - snprintf(cmd, BUFLEN, "volume decibels=%0.1f\n", decibels); - influx_insert(cmd, NULL); - return 0; + time_t now = time(NULL); + snprintf(cmd, BUFLEN, "volume decibels=%0.1f %ld\n", decibels, now); + if (doit) { + influx_insert(cmd, NULL); + } else { + info("Would have done: %s", cmd); + } + return TRUE; +} + +double getdecibels(uint8_t *buf) { + if (!buf) return -1; + if (strlen((char *)buf) < 2) return -1; + return (buf[0] << 8 | buf[1])/10; +} + +void handle_signal(int signum) { + exit(1); +} + +void cleanup(void) { + info("Cleaning up...\n"); + if (dev) hid_close(dev); + if (influxhost) free(influxhost); + if (influxdb) free(influxdb); + if (influxuser) free(influxuser); + if (influxpass) free(influxpass); + + hid_exit(); } int main(int argc, char *argv[]) { @@ -308,11 +494,28 @@ int main(int argc, char *argv[]) { //hid_device *handle; uint8_t capture_cmd[8] = { CMD_CAPTURE }; uint8_t buf[8]; - char retbuf[BUFLEN]; - int res; - int i; + char retbuf[BUFLEN],path[BUFLEN]; + int i, finished = 0; + int interval = 0; + int useinflux = FALSE; enum mode_enum { M_PROBE, M_TEST, M_TESTDB } mode = M_PROBE; - hid_device *dev = NULL; + struct timeval seed; + + gettimeofday(&seed, NULL); + + // generate magic id in bytes 2-4 + srand(seed.tv_usec); + for (i = 1; i <= 3; i++) { + capture_cmd[i] = rand() % 256; + } + for (i = 4; i <= 7; i++) { + capture_cmd[i] = 0; + } + printf("cap cmd is:\n"); + for (i = 0; i < 8; i++) { + printf(" 0x%01x\n",capture_cmd[i]); + } + printf("\n"); // handle args for (i=1; i= 2\n"); + exit(1); + } + } else { + err("Wait interval not provided\n"); + exit(1); + } + } else if (!strcmp(argv[i], "-y")) { + doit = TRUE; + } else if (!strcmp(argv[i], "-H")) { + i++; + if (i < argc && argv[i]) { + influxhost = strdup(argv[i]); + } else { + err("influxdb host not provided\n"); + exit(1); + } + } else if (!strcmp(argv[i], "-d")) { + i++; + if (i < argc && argv[i]) { + influxdb = strdup(argv[i]); + } else { + err("influxdb dbname not provided\n"); + exit(1); + } + } else if (!strcmp(argv[i], "-u")) { + i++; + if (i < argc && argv[i]) { + influxuser = strdup(argv[i]); + } else { + err("influxdb username not provided\n"); + exit(1); + } + } else if (!strcmp(argv[i], "-p")) { + i++; + if (i < argc && argv[i]) { + influxpass = strdup(argv[i]); + } else { + err("influxdb password not provided\n"); + exit(1); + } + } else { + err("Invalid argument '%s'\n", argv[i]); + exit(1); } } - influx_init("gridbug.nethack.net", "haven", "", ""); + if (atexit(cleanup)) { + perror("atexit()"); + exit(1); + } + signal(SIGINT, handle_signal); - if (mode == M_TESTDB) { - printf("running ping...\n"); - influx_ping(retbuf); - exit(1); + + if (!outmode) { + outmode = OM_SHORT; + } + + if (outmode & OM_DB) { + useinflux = TRUE; + } else if (mode == M_TESTDB) { + useinflux = TRUE; + } else { + useinflux = FALSE; + } + + if (useinflux) { + if (influx_init(influxhost, influxdb, influxuser, influxpass)) { + exit(1); + } + + if (mode == M_TESTDB) { + verbose = TRUE; + } + vinfo("Running influxdb http PING..."); + if (influx_ping(retbuf)) { + err("Couldn't acess influxdb at host=%s:8086 db=%s",influxhost,influxdb); + exit(1); + } + if (mode == M_TESTDB) { + exit(0); + } } if (hid_init()) { @@ -335,31 +635,63 @@ int main(int argc, char *argv[]) { err("Failed to initialise hidapi."); exit(1); } - dev = hid_open(MIC_VID, MIC_PID, NULL); + dev = hid_open(hextoint(MIC_VID), hextoint(MIC_PID), NULL); if (mode == M_TEST) { - if (dev) { - info("Found Benestar USB sound meter (vendor 0x%04hx, product 0x%04hx).", MIC_VID, MIC_PID); - exit(0); - } + verbose = TRUE; + } + if (dev) { + vinfo("Found Benestar USB sound meter (vendor 0x%04hx, product 0x%04hx).", MIC_VID, MIC_PID); + getdevpath(hextoint(MIC_VID), hextoint(MIC_PID), path); + vinfo("Full device path is: %s", path); } if (!dev) { err("Benestar USB sound meter not found (vendor 0x%04hx, product 0x%04hx).", MIC_VID, MIC_PID); exit(1); } - - // get current reading - res = hid_write(dev, capture_cmd, 9); - - // wait for response - // see: https://github.com/pvachon/gm1356/blob/master/splread.c - res = E_TIMEOUT; - while (res == E_TIMEOUT) { - res = readresult(dev, buf); + if (mode == M_TEST) { + exit(0); } - if (res == 0) { - showlevel(buf); + + while (!finished) { + int rv = 0,donereading = 0,ntimeouts = 0;; + vinfo("Asking for status"); + // get current reading + hid_write(dev, capture_cmd, 9); + + // wait for response + vinfo("Waiting for response"); + while (!donereading) { + // see: https://github.com/pvachon/gm1356/blob/master/splread.c + rv = readresult(dev, buf); + if (rv) { + // failure + ntimeouts++; + + if (ntimeouts >= 3) { + // give up for this time + donereading = 1; + } else { + if (ntimeouts == 2) { + // after first retry, try resetting the usb device + resetusb(path); + } + // try again + hid_write(dev, capture_cmd, 9); + } + } else { + // success + dooutput(buf); + donereading = 1; + } + } + + if (interval) { + sleep(interval); + } else { + finished = 1; + } } /* @@ -372,59 +704,56 @@ int main(int argc, char *argv[]) { except: None */ - hid_close(dev); - - hid_exit(); return 0; } /* - // Initialize the hidapi library - res = hid_init(); + // Initialize the hidapi library + res = hid_init(); - // Open the device using the VID, PID, - // and optionally the Serial number. - handle = hid_open(0x4d8, 0x3f, NULL); + // Open the device using the VID, PID, + // and optionally the Serial number. + handle = hid_open(0x4d8, 0x3f, NULL); - // Read the Manufacturer String - res = hid_get_manufacturer_string(handle, wstr, MAX_STR); - wprintf(L"Manufacturer String: %s\n", wstr); + // Read the Manufacturer String + res = hid_get_manufacturer_string(handle, wstr, MAX_STR); + wprintf(L"Manufacturer String: %s\n", wstr); - // Read the Product String - res = hid_get_product_string(handle, wstr, MAX_STR); - wprintf(L"Product String: %s\n", wstr); + // Read the Product String + res = hid_get_product_string(handle, wstr, MAX_STR); + wprintf(L"Product String: %s\n", wstr); - // Read the Serial Number String - res = hid_get_serial_number_string(handle, wstr, MAX_STR); - wprintf(L"Serial Number String: (%d) %s\n", wstr[0], wstr); + // Read the Serial Number String + res = hid_get_serial_number_string(handle, wstr, MAX_STR); + wprintf(L"Serial Number String: (%d) %s\n", wstr[0], wstr); - // Read Indexed String 1 - res = hid_get_indexed_string(handle, 1, wstr, MAX_STR); - wprintf(L"Indexed String 1: %s\n", wstr); + // Read Indexed String 1 + res = hid_get_indexed_string(handle, 1, wstr, MAX_STR); + wprintf(L"Indexed String 1: %s\n", wstr); - // Toggle LED (cmd 0x80). The first byte is the report number (0x0). - buf[0] = 0x0; - buf[1] = 0x80; - res = hid_write(handle, buf, 65); + // Toggle LED (cmd 0x80). The first byte is the report number (0x0). + buf[0] = 0x0; + buf[1] = 0x80; + res = hid_write(handle, buf, 65); - // Request state (cmd 0x81). The first byte is the report number (0x0). - buf[0] = 0x0; - buf[1] = 0x81; - res = hid_write(handle, buf, 65); + // Request state (cmd 0x81). The first byte is the report number (0x0). + buf[0] = 0x0; + buf[1] = 0x81; + res = hid_write(handle, buf, 65); - // Read requested state - res = hid_read(handle, buf, 65); + // Read requested state + res = hid_read(handle, buf, 65); - // Print out the returned buffer. - for (i = 0; i < 4; i++) - printf("buf[%d]: %d\n", i, buf[i]); + // Print out the returned buffer. + for (i = 0; i < 4; i++) + printf("buf[%d]: %d\n", i, buf[i]); - // Close the device - hid_close(handle); + // Close the device + hid_close(handle); - // Finalize the hidapi library - res = hid_exit(); + // Finalize the hidapi library + res = hid_exit(); */ diff --git a/howard.h b/howard.h index 7411012..d60a036 100644 --- a/howard.h +++ b/howard.h @@ -1,12 +1,19 @@ enum influxcmdtype { I_READ, I_WRITE, I_PING }; +char *getdevpath(int vid, int pid, char *retvar); +int resetusb(char *path); +char *append(char *orig, char *new, int maxlen); void colprintf( char *prefix, const char *col, char* format, va_list *args ); +double getdecibels(uint8_t *buf); +void cleanup(void); +void handle_signal(int signum); void info( char* format, ... ); +void vinfo( char* format, ... ); void err( char* format, ... ); void warn( char* format, ... ); int hextoint(char *hex); int readresult(hid_device *dev, uint8_t *retbuf); -void showlevel(uint8_t *buf); +void dooutput(uint8_t *buf); int tcpconnect(char *hname, int port); int dohttp(char *hname, int port, char *header, char *body, char *retbuf); struct hostent *resolve(char *hname);