diff --git a/README.md b/README.md index d901e02..f28df4f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Overview -Sound level monitoring via USB Benetech GM1356. +Performs sound level monitoring using a USB Benetech GM1356 sonometer. Named after [Loud Howard](https://dilbert.com/strip/1995-04-21). @@ -9,10 +9,39 @@ Big thanks to [Maciej Ciemborowicz](https://github.com/ciembor) for his work doc # Requirements - [hidapi](https://github.com/libusb/hidapi) - An OS which recognises the GM1356 as a USB device (ie. not OSX Big Sur). + - A GM1356 sonometer (obviously) # Usage -TBC +```text +bash$ ./howard -h +usage: howard [OPTIONS] outputmode1 [ outputmode2 ... ] + +Reads maximum noise level from a Benestar GM1356 sonometer +over a given interval, and optionally stores result in an +influxdb database. + +Valid output modes are: + short (single line to stdout) + long (multiple lines to stdout) + db (HTTP POST to influxdb, must also provide INFLUXDB OPTIONS - see below) + +OPTIONS: + -c Run endlessly (also see -i and -w) + -h Show this usage text then exit. + -i secs Define time over which to get max decibels + -t Test mode - just probe for sononeter then exit + -T Influxdb test mode - just test influxdb connectivity then exit + -w secs Define period to wait between noise level checks + insert will just be written to stdout. + +INFLUXDB OPTIONS: + -d dbname Define influxdb database name + -H hostname Define influxdb hostname + -p password Define influxdb password + -u username Define influxdb password + -y Actually write to influxdb. Without this, +``` # Screenshots @@ -20,4 +49,11 @@ 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`. + - 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`. + + - I don't yet have configuration commands working, so the following need to be manually configured on the physical device: + - slow/fast exponential time averaging + - MAX mode + - dBA vs dBC units + + diff --git a/howard.c b/howard.c index e7be98a..a42e39e 100644 --- a/howard.c +++ b/howard.c @@ -31,22 +31,6 @@ #include #include "howard.h" -#define TRUE (-1) -#define FALSE (0) - -#define BUFLEN 8196 -#define BUFLENSMALL 255 - -// Benestar sound meter -//#define MIC_VID (0x1630) -//#define MIC_PID (0x05dc) -#define MIC_VID "64bd" -#define MIC_PID "74e3" - -#define CMD_CAPTURE (0xb3) - -hid_device *dev = NULL; - const char *rangestr[] = { "30-130", "30-80", @@ -55,38 +39,77 @@ const char *rangestr[] = { "80-130", }; +enum run_mode mode = M_PROBE; + +// globals +hid_device *dev = NULL; // mic device +int useinflux = FALSE; // write results to influxdb? +int continuous = FALSE; // continue after 1st iteration? +int verbose = FALSE; // extra logging + +int waitsecs = 3; // time to wait between reads +int itersecs = 60; // time to gather max decibels over +int outmode = 0; // bitmask using OM_* macros +uint8_t config_cmd[17]; // config string based on outmode +uint8_t capture_cmd[9] = { CMD_CAPTURE }; +int doit = FALSE; // really write to influxdb? +char devpath[BUFLEN]; // procfs path to mic (for usb resets) + +// influxdb parameters char *influxhost = NULL; char *influxdb = NULL; char *influxuser = NULL; char *influxpass = NULL; -int doit = FALSE; -int verbose = FALSE; -int outmode = 0; +// functions +int add_output_mode(char *m) { + if (!strcmp(m, "db")) { + outmode |= OM_DB; + } else if (!strcmp(m, "short")) { + outmode |= OM_SHORT; + } else if (!strcmp(m, "long")) { + outmode |= OM_LONG; + } else { + err("Invalid output mode '%s'", m); + return TRUE; + } + return FALSE; +} -#define OM_SHORT (1) -#define OM_LONG (2) -#define OM_DB (4) +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; +} -#define FLAG_DBCMODE (0x10) -#define FLAG_FASTMODE (0x40) +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); -#define E_TIMEOUT -1 -#define E_BADREAD -2 + hid_exit(); +} -// ANSI stuff -#define BOLD "\x1b[1m" -#define ITALIC "\x1b[3m" -#define STRIKE "\x1b[9m" -#define PLAIN "\x1b[0m" -#define UNDERLINE "\x1b[4m" -#define RED "\x1b[31m" -#define MAGENTA "\x1b[35m" -#define GREEN "\x1b[32m" -#define YELLOW "\x1b[33m" -#define BLUE "\x1b[34m" -#define CYAN "\x1b[36m" -#define GREY "\x1b[2;37m" +void clear_results(result_t *data) { + data->dirty = 1; + data->unixtime = 0; + data->flags = 0; + data->decibels = 0; + data->rangeidx = 0; + bzero(data->range, BUFLENSMALL); + bzero(data->dbunits, BUFLENSMALL); + bzero(data->checkmode, BUFLENSMALL); + bzero(data->timenice, BUFLENSMALL); +} void colprintf( char *prefix, const char *col, char *format, va_list *args ) { printf("%s%s%s %s%s",col,BOLD,prefix,PLAIN,col); @@ -94,7 +117,82 @@ void colprintf( char *prefix, const char *col, char *format, va_list *args ) { printf("%s\n",PLAIN); } -char *getdevpath(int vid, int pid, char *retvar) { +// TODO: this doesn't work yet +int configure_mic(void) { + int res,rv = 1; + uint8_t buf[BUFLENSMALL]; + vinfo("noop config command"); + return 0; + vinfo("Sending config command"); + hid_write(dev, config_cmd, 17); + res = read_single_result(dev, buf); + if (res == 0) { + if (buf[0] == RES_ACK) { + vinfo("Got config ACK"); + rv = 0; + } else { + vinfo("Got non-ack config response"); + } + } else { + vinfo("%s after config", (res == E_TIMEOUT) ? "Timeout" : "Bad read" ); + } + return rv; +} + +struct hostent *dnslookup(char *hname) { + struct hostent *hptr; + hptr = gethostbyname(hname); + if (hptr == NULL) { + err("Failed to dnslookup %s (%s)",hname,hstrerror(h_errno)); + return NULL; + } + if (hptr->h_addrtype != AF_INET) { + err("No IN records for %s.",hname); + return NULL; + } + if (hptr->h_addr_list == NULL) { + err("No A records for %s.",hname); + return NULL; + } + return hptr; +} + +void err( char* format, ... ) { + va_list args; + va_start(args, format); + colprintf( "ERROR:", RED, format, &args); + va_end( args ); +} + +double extract_decibels(uint8_t *buf) { + if (!buf) return -1; + if (strlen((char *)buf) < 2) return -1; + return (buf[0] << 8 | buf[1])/10; +} + +void generate_commands(void) { + struct timeval seed; + int i; + // command to ask for a capture + // bytes 2-4 must be unique for mic to accept command + gettimeofday(&seed, NULL); + 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; + } + + // command to configure mic + bzero(config_cmd, 17); + config_cmd[0] = CMD_CONFIGURE; // } config + //config_cmd[1] ^= FLAG_FASTMODE; // not fast mode + config_cmd[1] |= FLAG_MAXMODE; // max mode + config_cmd[1] = FLAG_RANGE_30_130; +} + +char *getprocfspath(int vid, int pid, char *retvar) { struct libusb_device **devlist; struct libusb_context *ctx; int i,ndevices,found = 0; @@ -128,190 +226,44 @@ char *getdevpath(int vid, int pid, char *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); - colprintf( "*", CYAN, format, &args); - 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); - colprintf( "ERROR:", RED, format, &args); - va_end( args ); -} - -void warn( char* format, ... ) { - va_list args; - va_start(args, format); - colprintf( "WARNING:", YELLOW, format, &args); - va_end( args ); +void handle_signal(int signum) { + exit(1); // (ie. call cleanup()) } int hextoint(char *hex) { int base=16; - if (strstr(hex, "0x") == hex) { - base=0; - } + if (strstr(hex, "0x") == hex) base=0; return strtol(hex, NULL, base); } -int readresult(hid_device *dev, uint8_t *retbuf) { - int totbytes = 0; - int wantbytes = 8; - time_t starttime; - double timeoutsecs = 0.5; - double timeoutms = timeoutsecs*1000; - starttime = time(NULL); +int influx_cmd(enum influxcmdtype cmdtype, char *cmd, char *retbuf) { + char header[BUFLEN]; + char newcmd[BUFLEN+2]; + int rc = -1; - while (totbytes != wantbytes) { - int thisbytes = 0; - thisbytes = hid_read_timeout(dev, &retbuf[totbytes], wantbytes + 1 - totbytes, timeoutms); - if (thisbytes < 0) { - warn("Incomplete read from usb (got %d bytes, want %d bytes): %ls",totbytes,wantbytes, hid_error(dev)); - return E_BADREAD; - } - - totbytes += thisbytes; - 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; - } + if (cmdtype == I_WRITE) { + 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); + snprintf(newcmd,BUFLEN,"q=%s",cmd); + } else if (cmdtype == I_PING) { + snprintf(header, BUFLEN, "GET /ping HTTP/1.1\r\nHost: influx:8086\r\n\r\n"); + strncpy(newcmd,"",BUFLEN); + } else { + err("Invalid influx command type (%d)", cmdtype); + return FALSE; } - return 0; + + rc = influx_httppost(influxhost, 8086, header, newcmd, retbuf); + if (rc != 204) { + err("Influx POST to http://%s:8086 db %s failed (HTTP %d)",influxhost,influxdb,rc); + return TRUE; + } + return FALSE; } -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"; - 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); - - 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 -1; - - sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { -err("socket"); - perror("socket()"); - return -1; - } - servaddr.sin_family = hptr->h_addrtype; - 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))) { - 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; -} - -int dohttp(char *hname, int port, char *header, char *body, char *retbuf) { +int influx_httppost(char *hname, int port, char *header, char *body, char *retbuf) { char response[BUFLEN]; char *p,*dp,retcode_str[BUFLENSMALL]; ssize_t n; @@ -368,36 +320,6 @@ int dohttp(char *hname, int port, char *header, char *body, char *retbuf) { return retcode_int; } -struct hostent *resolve(char *hname) { - struct hostent *hptr; - hptr = gethostbyname(hname); - if (hptr == NULL) { - err("Failed to resolve %s (%s)",hname,hstrerror(h_errno)); - return NULL; - } - if (hptr->h_addrtype != AF_INET) { - err("No IN records for %s.",hname); - return NULL; - } - if (hptr->h_addr_list == NULL) { - err("No A records for %s.",hname); - return NULL; - } - 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) { char missing[BUFLEN]; strncpy(missing, "", BUFLEN); @@ -412,50 +334,20 @@ int influx_init(char *hname, char *db, char *user, char *pass) { return FALSE; } -int influx_cmd(enum influxcmdtype cmdtype, char *cmd, char *retbuf) { - char header[BUFLEN]; - char newcmd[BUFLEN+2]; - int rc = -1; - - if (cmdtype == I_WRITE) { - 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); - snprintf(newcmd,BUFLEN,"q=%s",cmd); - } else if (cmdtype == I_PING) { - snprintf(header, BUFLEN, "GET /ping HTTP/1.1\r\nHost: influx:8086\r\n\r\n"); - strncpy(newcmd,"",BUFLEN); - } else { - err("Invalid influx command type (%d)", cmdtype); - return FALSE; - } - - rc = dohttp(influxhost, 8086, header, newcmd, retbuf); - if (rc != 204) { - err("Influx write to %s:8086 db %s failed (HTTP %d)",influxhost,influxdb,rc); - return TRUE; - } - return FALSE; -} - int influx_insert(char *cmd, char *retbuf) { return influx_cmd(I_WRITE, cmd, retbuf); } -int influx_query(char *cmd, char *retbuf ) { - return influx_cmd(I_READ, cmd, retbuf); -} - int influx_ping(char *retbuf) { int rv; - rv = influx_cmd(I_PING, "", retbuf); - if (!rv) { - vinfo("Ping success"); - } + if (!(rv = influx_cmd(I_PING, "", retbuf))) vinfo("Ping success"); return rv; } +int influx_query(char *cmd, char *retbuf ) { + return influx_cmd(I_READ, cmd, retbuf); +} + int influx_write_decibels(double decibels) { char cmd[BUFLEN]; time_t now = time(NULL); @@ -463,297 +355,430 @@ int influx_write_decibels(double decibels) { if (doit) { influx_insert(cmd, NULL); } else { - info("Would have done: %s", cmd); + pr(GREEN, "Proposed influx cmd (use -y to do writes): %s", cmd); } - return TRUE; + return FALSE; } -double getdecibels(uint8_t *buf) { - if (!buf) return -1; - if (strlen((char *)buf) < 2) return -1; - return (buf[0] << 8 | buf[1])/10; +void info( char* format, ... ) { + va_list args; + va_start(args, format); + colprintf( "*", CYAN, format, &args); + va_end( args ); } -void handle_signal(int signum) { - exit(1); +int init_influxdb(void) { + char retbuf[BUFLEN]; + if (!useinflux) return TRUE; + if (influx_init(influxhost, influxdb, influxuser, influxpass)) return TRUE; + + vinfo("Running influxdb http PING..."); + if (influx_ping(retbuf)) { + err("Couldn't access influxdb at host=%s:8086 db=%s",influxhost,influxdb); + return TRUE; + } + return FALSE; } -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); +int init_mic(void) { + if (hid_init()) { + err("Failed to initialise hidapi. Details:"); + perror("hid_init()"); + return TRUE; + exit(1); + } + dev = hid_open(hextoint(MIC_VID), hextoint(MIC_PID), NULL); + if (dev) { + vinfo("Found Benestar USB sound meter (vendor 0x%04hx, product 0x%04hx).", MIC_VID, MIC_PID); + getprocfspath(hextoint(MIC_VID), hextoint(MIC_PID), devpath); + vinfo("Full device path is: %s", devpath); + } else { + err("Benestar USB sound meter not found (vendor 0x%04hx, product 0x%04hx).", MIC_VID, MIC_PID); + return TRUE; + } + return FALSE; +} - hid_exit(); +int mic_docmd(hid_device *dev, uint8_t *cmd, int cmdlen, uint8_t *buf) { + int rv,ntimeouts = 0, donereading = 0; + vinfo("Sending command"); + hid_write(dev, cmd, cmdlen); + vinfo("Waiting for response"); + while (!donereading) { + rv = read_single_result(dev, buf); + if (rv) { + // failure + if (++ntimeouts >= 3) { + // too many timeouts, give up + return TRUE; + } else { + if (ntimeouts == 2) { + // after first retry, try resetting the usb device + resetusb(devpath); + configure_mic(); + } + // try again + hid_write(dev, cmd, cmdlen); + } + } else { + // success + donereading = 1; + } + } + return FALSE; +} + +int output_results(result_t *data) { + char *strformat = "%12s: %s\n"; + char *dbformat = "%12s: %4.2f %s\n"; + char *timeformat = "%12s: %ld (%s)\n"; + if (!data) return TRUE; + if (data->dirty == 0) return TRUE; + if (outmode & OM_LONG) { + pr(GREEN,timeformat, "Time", data->unixtime, data->timenice); + pr(GREEN, dbformat, "Level", data->decibels, data->dbunits); + pr(GREEN, strformat, "Mode", data->checkmode); + pr(GREEN, strformat, "Range", data->range); + } + if (outmode & OM_SHORT) { + pr(GREEN, "%s time=[%ld] level=[%4.2f %s] mode=[%s] range=[%s]\n", + data->timenice, + data->unixtime, + data->decibels, + data->dbunits, + data->checkmode, + data->range); + } + if (outmode & OM_DB) { + influx_write_decibels(data->decibels); + } + return FALSE; +} + +int parse_results(uint8_t *buf, result_t *data) { + struct tm *nowlocal; + double decibels; + if (!data || !buf) return TRUE; + + decibels = extract_decibels(buf); + if (decibels < 0 || decibels > 130) { + warn("Ignoring out-of-range decibel reading: %0.2f", decibels); + return TRUE; + } + data->dirty = 1; + if (decibels > data->decibels) { + data->decibels = decibels; + } + data->flags = buf[2]; + data->rangeidx = buf[2] & 0xf; + time(&(data->unixtime)); + nowlocal = localtime(&(data->unixtime)); + strftime(data->timenice, BUFLENSMALL, "%d/%b/%Y %H:%M:%S", nowlocal); + strncpy(data->dbunits, data->flags & FLAG_DBCMODE ? "dBC" : "dBA", BUFLENSMALL); + strncpy(data->checkmode, data->flags & FLAG_FASTMODE ? "Fast" : "Slow", BUFLENSMALL); + if (data->flags & FLAG_MAXMODE) strncat(data->checkmode, "-MAX", 5); + strncpy(data->range, data->rangeidx > 0x4 ? "unknown" : rangestr[data->rangeidx], BUFLENSMALL); + return FALSE; +} + +void pr(char *col, char* format, ... ) { + va_list args; + va_start(args, format); + colprintf( "==>", col, format, &args); + va_end( args ); +} + +enum read_status read_single_result(hid_device *dev, uint8_t *retbuf) { + int totbytes = 0; + int wantbytes = 8; + time_t starttime; + 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) { + warn("Incomplete read from usb (got %d bytes, want %d bytes): %ls",totbytes,wantbytes, hid_error(dev)); + return E_BADREAD; + } + + totbytes += thisbytes; + if (thisbytes > 0) { + vinfo("Got %d/%d bytes",totbytes,wantbytes); + } + + if (totbytes > 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; + } + } + return E_NOERROR; +} + +int resetusb(char *filename) { + int fd,rv = FALSE; + strncpy(filename, devpath, 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 = TRUE; + } else { + int rc; + rc = ioctl(fd, USBDEVFS_RESET, 0); + if (rc < 0) { + err("Failed to send reset, details:", filename); + perror("ioctl()"); + rv = TRUE; + } else { + info("Reset successful"); + rv = FALSE; + } + close(fd); + } + return rv; +} + +int tcpconnect(char *hname, int port) { + int sockfd; + struct sockaddr_in servaddr; + struct hostent *hptr; + fd_set fdset; + struct timeval tv; + int tcpflags; + + hptr = dnslookup(hname); + if (!hptr) return BADFD; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) { + err("Couldn't create socket. Details:"); + perror("socket()"); + return BADFD; + } + servaddr.sin_family = hptr->h_addrtype; + 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)); + + // go to non-blocking mode so that we can + // use select() to handle syn timeouts + fcntl(sockfd, F_SETFL, O_NONBLOCK); + + if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr))) { + 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 BADFD; + } + } + } else { + perror("tcpconnect()"); + return BADFD; + } + } + + // go back to blocking mode + tcpflags = fcntl(sockfd, F_GETFL); + fcntl(sockfd, F_SETFL, tcpflags & ~O_NONBLOCK); + + return sockfd; +} + +void usage(void) { + printf("usage: howard [OPTIONS] outputmode1 [ outputmode2 ... ]\n"); + printf("\n"); + printf("Reads maximum noise level from a Benestar GM1356 sonometer\n"); + printf("over a given interval, and optionally stores result in an\n"); + printf("influxdb database.\n"); + printf("\n"); + printf("Valid output modes are:\n"); + printf(" short (single line to stdout)\n"); + printf(" long (multiple lines to stdout)\n"); + printf(" db (HTTP POST to influxdb, must also provide INFLUXDB OPTIONS - see below)\n"); + printf("\n"); + printf("OPTIONS:\n"); + printf(" -c Run endlessly (also see -i and -w)\n"); + printf(" -h Show this usage text then exit.\n"); + printf(" -i secs Define time over which to get max decibels\n"); + printf(" -t Test mode - just probe for sononeter then exit\n"); + printf(" -T Influxdb test mode - just test influxdb connectivity then exit\n"); + printf(" -w secs Define period to wait between noise level checks\n"); + printf(" insert will just be written to stdout.\n"); + printf("\n"); + printf("INFLUXDB OPTIONS:\n"); + printf(" -d dbname Define influxdb database name\n"); + printf(" -H hostname Define influxdb hostname\n"); + printf(" -p password Define influxdb password\n"); + printf(" -u username Define influxdb password\n"); + printf(" -y Actually write to influxdb. Without this,\n"); + printf("\n"); +} + +int validateargs(void) { + if (itersecs <= waitsecs) { + err("Iteration time must be greater than wait time.\n"); + return TRUE; + } + + if (!outmode && mode == M_PROBE) { + err("No output modes provided."); + return TRUE; + } + + if ((outmode & OM_DB) || (mode == M_TESTDB)) { + useinflux = TRUE; + } else { + useinflux = FALSE; + } + return FALSE; +} + +// output only in verbose mode +void vinfo( char* format, ... ) { + va_list args; + if (!verbose) return; + va_start(args, format); + colprintf( "*", CYAN, format, &args); + va_end( args ); +} + +void warn( char* format, ... ) { + va_list args; + va_start(args, format); + colprintf( "WARNING:", YELLOW, format, &args); + va_end( args ); } int main(int argc, char *argv[]) { - //unsigned char buf[65]; - //hid_device *handle; - uint8_t capture_cmd[8] = { CMD_CAPTURE }; - uint8_t buf[8]; - 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; - 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"); + uint8_t resultbuf[9]; + int finished = 0, opt; + result_t data; // 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); - } + break; + case 'y': + doit = TRUE; + break; + case ':': + err("%s requires a value\n", optopt); + break; + case '?': + err("Invalid argument '%s'\n", optopt); + break; + } + } + + for(;optind < argc; optind++){ + if (add_output_mode(argv[optind])) exit(1); } + + if (mode == M_TEST || mode == M_TESTDB) verbose = TRUE; + + if (validateargs()) exit(1); + if (atexit(cleanup)) { perror("atexit()"); exit(1); } signal(SIGINT, handle_signal); + if (mode == M_TESTDB) exit(init_influxdb()); + if (mode == M_TEST) exit(init_mic()); - if (!outmode) { - outmode = OM_SHORT; - } + generate_commands(); - if (outmode & OM_DB) { - useinflux = TRUE; - } else if (mode == M_TESTDB) { - useinflux = TRUE; - } else { - useinflux = FALSE; - } + if (init_influxdb()) exit(1); + if (init_mic()) exit(1); - 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()) { - perror("hid_init()"); - err("Failed to initialise hidapi."); - exit(1); - } - dev = hid_open(hextoint(MIC_VID), hextoint(MIC_PID), NULL); - - if (mode == M_TEST) { - 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); - } - if (mode == M_TEST) { - exit(0); - } + if (configure_mic()) exit(1); while (!finished) { - int rv = 0,donereading = 0,ntimeouts = 0;; - vinfo("Asking for status"); - // get current reading - hid_write(dev, capture_cmd, 9); + int donereading = 0; + time_t iterstart; - // wait for response - vinfo("Waiting for response"); + clear_results(&data); + // use max decibel reading over 'itersecs' seconds + iterstart = time(NULL); 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 { + if (!mic_docmd(dev, capture_cmd, 9, resultbuf)) { // success - dooutput(buf); + parse_results(resultbuf, &data); + } + if (data.dirty) { + pr(MAGENTA, "Current max: %0.2f %s (%d seconds left)", data.decibels, data.dbunits, itersecs - (time(NULL) - iterstart)); + } + + vinfo("Delaying for %d seconds before next read", waitsecs); + sleep(waitsecs); + + // have 'itersecs' passed? + if ((time(NULL) - iterstart) >= itersecs) { donereading = 1; } } - if (interval) { - sleep(interval); - } else { - finished = 1; - } - } + // output to screen and/or influxdb + output_results(&data); -/* - ret = dev.ctrl_transfer(0xC0,4,0,0,200) - dB = (ret[0]+((ret[1]&3)*256))*0.1+30 - print dB - msg="{'dB':'"+str(dB)+"'}" - try: - requests.post('https://temporacloud.com/connection/clientSend', data={'streams':streams,'tokens':tokens,'message':msg},verify=False) - except: None -*/ + if (!continuous) finished = 1; + } return 0; } - - -/* - - // 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); - - // 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 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); - - // 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); - - // 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]); - - // Close the device - hid_close(handle); - - // Finalize the hidapi library - res = hid_exit(); -*/ - diff --git a/howard.h b/howard.h index d60a036..1e3c03c 100644 --- a/howard.h +++ b/howard.h @@ -1,25 +1,115 @@ +// booleans +#define TRUE (-1) +#define FALSE (0) +#define BADFD (-1) + +// buffer lengths +#define BUFLEN 8196 +#define BUFLENSMALL 255 + +// Benestar sound meter +#define MIC_VID "64bd" +#define MIC_PID "74e3" + +// mic constants +#define FLAG_DBCMODE (0b00010000) // use dBc (not dBa) +#define FLAG_MAXMODE (0b00100000) // get max db over interval +// fast = 125ms exp time avg +// slow = 1000ms exp time avg +#define FLAG_FASTMODE (0b01000000) +// ranges supported by sonometer +#define FLAG_RANGE_30_130 (0b00000000) +#define FLAG_RANGE_30_80 (0b00000001) +#define FLAG_RANGE_50_100 (0b00000010) +#define FLAG_RANGE_60_110 (0b00000011) +#define FLAG_RANGE_80_130 (0b00000100) + +// ANSI stuff +#define BOLD "\x1b[1m" +#define ITALIC "\x1b[3m" +#define STRIKE "\x1b[9m" +#define PLAIN "\x1b[0m" +#define UNDERLINE "\x1b[4m" +#define RED "\x1b[31m" +#define MAGENTA "\x1b[35m" +#define GREEN "\x1b[32m" +#define YELLOW "\x1b[33m" +#define BLUE "\x1b[34m" +#define CYAN "\x1b[36m" +#define GREY "\x1b[2;37m" + +// Output modes +#define OM_SHORT (1) // Print to stdout in one line +#define OM_LONG (2) // Print to stdout over multiple lines +#define OM_DB (4) // Write to influxdb + + +// Control strings +#define CMD_CAPTURE (0xb3) // take a reading +#define CMD_CONFIGURE (0x56) // apply settings +#define RES_ACK (0xc4) // ack for command + +// what to do +enum run_mode { + M_PROBE, // measure noise level + M_TEST, // just check for sononeter then exit + M_TESTDB // just check influxdb access then exit +}; + +// return codes for read_single_result() +enum read_status { + E_NOERROR=0, // all good + E_TIMEOUT=-1, // didn't receive all data in time + E_BADREAD=-2 // received too much or too little data +}; + + +typedef struct { + int dirty; + time_t unixtime; + uint8_t flags; + double decibels; + int rangeidx; + char range[BUFLENSMALL]; + char dbunits[BUFLENSMALL]; + char checkmode[BUFLENSMALL]; + char timenice[BUFLENSMALL]; +} result_t; enum influxcmdtype { I_READ, I_WRITE, I_PING }; -char *getdevpath(int vid, int pid, char *retvar); -int resetusb(char *path); + +// prototypes +int add_output_mode(char *m); 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 clear_results(result_t *data); +void colprintf( char *prefix, const char *col, char* format, va_list *args ); +int configure_mic(void); +struct hostent *dnslookup(char *hname); void err( char* format, ... ); -void warn( char* format, ... ); +double extract_decibels(uint8_t *buf); +void generate_commands(void); +char *getprocfspath(int vid, int pid, char *retvar); +void handle_signal(int signum); int hextoint(char *hex); -int readresult(hid_device *dev, uint8_t *retbuf); -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); -int influx_init(char *hname, char *db, char *user, char *pass); int influx_cmd(enum influxcmdtype cmdtype, char *cmd, char *retbuf); +int influx_httppost(char *hname, int port, char *header, char *body, char *retbuf); +int influx_init(char *hname, char *db, char *user, char *pass); int influx_insert(char *cmd, char *retbuf); -int influx_query(char *cmd, char *retbuf); int influx_ping(char *retbuf); +int influx_query(char *cmd, char *retbuf); int influx_write_decibels(double decibels); +void info( char* format, ... ); +int init_influxdb(void); +int init_mic(void); +int mic_docmd(hid_device *dev, uint8_t *cmd, int cmdlen, uint8_t *buf); +int output_results(result_t *data); +int parse_results(uint8_t *buf, result_t *data); +void pr(char *col, char* format, ... ); +enum read_status read_single_result(hid_device *dev, uint8_t *retbuf); +int resetusb(char *path); +int tcpconnect(char *hname, int port); +void usage(void); +int validateargs(void); +void vinfo( char* format, ... ); +void warn( char* format, ... );