/* * visomat-data-downloader - download data from Visomat Double Comfort * * The Visomat Double Comfort is a blood pressure meter with a USB port. * * Copyright (C) 2013 Antonio Ospite * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #ifdef DEBUG #define debug(...) fprintf(stderr, __VA_ARGS__) static void debug_dump_buffer(const char *filename, uint8_t *buffer, unsigned int len) { FILE *dump_file; dump_file = fopen(filename, "wb"); if (dump_file == NULL) { fprintf(stderr, "Failed to open %s: %s\n", filename, strerror(errno)); return; } fwrite(buffer, 1, len, dump_file); fclose(dump_file); } #else #define debug(...) do {} while(0) static void debug_dump_buffer(const char *filename, uint8_t *buffer, unsigned int len) { (void)filename; (void)buffer; (void)len; } #endif #define VISOMAT_DEVICE_VID 0x1247 #define VISOMAT_DEVICE_PID 0x00f8 #define VISOMAT_CONFIGURATION 1 #define VISOMAT_INTERFACE 1 #define VISOMAT_EP_IN 0x82 #define VISOMAT_EP_OUT 0x03 #define VISOMAT_PACKET_SIZE 64 #define STX 0x02 #define ETX 0x03 #define DATETIME_PACKET_SIZE 15 #define BASE_YEAR 2000 typedef enum { VISOMAT_CMD_DUMP_EEPROM, VISOMAT_CMD_UNKNOWN1, /* maybe the firmware revision */ VISOMAT_CMD_GET_DATETIME, VISOMAT_CMD_START_MEASURE1, VISOMAT_CMD_UNKNOWN2, /* XXX transmission hangs */ VISOMAT_CMD_UNKNOWN3, } visomat_command; static char command_codes[][3] = { [VISOMAT_CMD_DUMP_EEPROM] = "R00", [VISOMAT_CMD_UNKNOWN1] = "R01", [VISOMAT_CMD_GET_DATETIME] = "R02", [VISOMAT_CMD_START_MEASURE1] = "R03", [VISOMAT_CMD_UNKNOWN2] = "R04", [VISOMAT_CMD_UNKNOWN3] = "R05", }; struct datetime { unsigned int year; unsigned int month; unsigned int day; unsigned int hour; unsigned int minute; }; struct pressure { unsigned int flag; /* XXX Maybe this means arrhythmia? */ unsigned int systolic; unsigned int diastolic; unsigned int pulses; }; static inline int extract_datetime(uint8_t *buffer, struct datetime *d) { int ret; ret = sscanf((char *)buffer, "%02u%02u%02u%02u%02u", &d->year, &d->month, &d->day, &d->hour, &d->minute); if (ret != 5) return -EINVAL; d->year += BASE_YEAR; return 0; } /* * NOTE: the CSV output is meant to be compatible with one from the Windows * program, which uses the %d/%m/%Y date format, so that's not my fault. */ static void print_record_csv_compat(struct datetime *d, struct pressure *p) { unsigned int pulse_pressure; printf("%02u/%02u/%04u;%02u.%02u;", d->day, d->month, d->year, d->hour, d->minute); printf("%u;%u;%u;", p->systolic, p->diastolic, p->pulses); pulse_pressure = p->systolic - p->diastolic; printf("%u;", pulse_pressure); if (p->flag) printf("x"); #if 0 /* The original software does not seem to be doing that */ if (p->pulses > 100) printf("tachycardia"); else if (p->pulses < 60) printf("bradycardia"); #endif printf("\n"); } /* TODO: it would be better to separate decoding data from printing it */ static int decode_eeprom(uint8_t *buffer, unsigned int len, unsigned int user_mask) { int ret; unsigned int n; unsigned int i; unsigned int j; unsigned int user_id; unsigned int num_records; struct datetime d; struct pressure p; if (buffer[0] != STX || buffer[1] != 'M') { fprintf(stderr, "Usupported data.\n"); return -EINVAL; } if (buffer[len - 1] != ETX) { fprintf(stderr, "Bad terminator in data. Buffer too small?\n"); return -EINVAL; } /* skip the initial STX */ i = 1; /* and the final ETX */ n = len - 1; while (i < n) { /* Each user record begins with 'M', maybe for "Memory" */ if (buffer[i] == 'M') { /* i tracks the bytes consumed */ i += 1; ret = sscanf((char *)(buffer + i), "%1u%02u", &user_id, &num_records); if (ret != 2) return -EINVAL; /* user_id and num_records take 3 bytes */ i += 3; /* * when there are no records, there is a dummy byte * which has to be consumed */ if (num_records == 0) i += 1; for (j = 0; j < num_records; j++) { ret = extract_datetime(buffer + i, &d); if (ret < 0) return ret; /* datetime takes 10 bytes */ i += 10; ret = sscanf((char *)(buffer + i), "%1u%03u%03u%03u", &p.flag, &p.systolic, &p.diastolic, &p.pulses); if (ret != 4) return -EINVAL; /* pressure data is 10 bytes */ i += 10; /* TODO: split out the printing part */ if (user_id & user_mask) { if (j == 0) printf("# User: %d\n", user_id); print_record_csv_compat(&d, &p); } } } } return 0; } static int decode_datetime(uint8_t *buffer, unsigned int len) { int ret; uint8_t code[4] = { 0 }; /* the initial STX + 3 bytes command code */ struct datetime d; uint8_t *pbuffer = buffer; if (len != DATETIME_PACKET_SIZE) return -EINVAL; code[0] = buffer[1]; code[1] = buffer[2]; code[2] = buffer[3]; ret = extract_datetime(pbuffer + sizeof(code), &d); if (ret < 0) return ret; printf("# (%s) Date: %04d/%02d/%02d %02d:%02d\n", code, d.year, d.month, d.day, d.hour, d.minute); return 0; } static int send_command(libusb_device_handle *dev, visomat_command cmd) { int ret; int transferred; uint8_t request[5]; request[0] = STX; request[1] = command_codes[cmd][0]; request[2] = command_codes[cmd][1]; request[3] = command_codes[cmd][2]; request[4] = ETX; transferred = 0; ret = libusb_bulk_transfer(dev, VISOMAT_EP_OUT, request, sizeof(request), &transferred, 0); if (ret != 0 || transferred != sizeof(request)) { fprintf(stderr, "Error: sending request: %d (%s)\ttransferred: %d (expected %zu)\n", ret, libusb_error_name(ret), transferred, sizeof(request)); return ret; } return 0; } static int get_response(libusb_device_handle *dev, uint8_t *buffer, unsigned int len) { int ret; int transferred; uint8_t response[VISOMAT_PACKET_SIZE] = { 0 }; unsigned int i; i = 0; do { unsigned int j; transferred = 0; ret = libusb_bulk_transfer(dev, VISOMAT_EP_IN, response, sizeof(response), &transferred, 5000); if (ret != 0) { fprintf(stderr, "Error getting response: %d (%s)\ttransferred: %d (expected %zu)\n", ret, libusb_error_name(ret), transferred, sizeof(response)); return ret; } for (j = 0; j < (unsigned int)transferred; j++) { buffer[i] = response[j]; i += 1; } } while (buffer[i - 1] != ETX && i < len); /* Check if the buffer contains a valid response packet */ if (buffer[0] != STX || buffer[i - 1] != ETX) return -EINVAL; return i; } /* Candidates for a future public API, if a shared library will ever be made */ #define visomat_device libusb_device_handle static int visomat_dump_eeprom(visomat_device *dev, unsigned int user_mask) { /* Assuming an EEPROM of 4 KiB */ uint8_t buffer[4096] = { 0 }; int ret; ret = send_command(dev, VISOMAT_CMD_DUMP_EEPROM); if (ret < 0) return ret; ret = get_response(dev, buffer, sizeof(buffer)); debug("buffer size: %d\n", ret); debug_dump_buffer("eeprom.bin", buffer, sizeof(buffer)); if (ret < 0) return ret; ret = decode_eeprom(buffer, ret, user_mask); if (ret < 0) return ret; return 0; } static int visomat_get_datetime(visomat_device *dev) { uint8_t buffer[255] = { 0 }; int ret; ret = send_command(dev, VISOMAT_CMD_GET_DATETIME); if (ret < 0) return ret; ret = get_response(dev, buffer, sizeof(buffer)); if (ret < 0) return ret; ret = decode_datetime(buffer, ret); if (ret < 0) return ret; return 0; } static void usage(const char *name) { printf("usage: %s [OPTIONS]\n\n", name); printf("OPTIONS:\n"); printf("\t-D\t\tenable libusb debug output\n"); printf("\t-h\t\tthis help message\n"); } int main(int argc, char *argv[]) { int ret; int opt; bool enable_libusb_debug = false; libusb_device_handle *dev; int current_configuration; while ((opt = getopt(argc, argv, "Dh")) != -1) { switch (opt) { case 'D': enable_libusb_debug = true; break; case 'h': usage(argv[0]); ret = 0; goto out; default: /* '?' */ usage(argv[0]); ret = -EINVAL; goto out; } } ret = libusb_init(NULL); if (ret < 0) { fprintf(stderr, "libusb_init failed: %s\n", libusb_error_name(ret)); goto out; } libusb_set_debug(NULL, enable_libusb_debug ? LIBUSB_LOG_LEVEL_DEBUG : LIBUSB_LOG_LEVEL_INFO); dev = libusb_open_device_with_vid_pid(NULL, VISOMAT_DEVICE_VID, VISOMAT_DEVICE_PID); if (dev == NULL) { fprintf(stderr, "libusb_open failed: %s\n", strerror(errno)); ret = -errno; goto out_libusb_exit; } current_configuration = -1; ret = libusb_get_configuration(dev, ¤t_configuration); if (ret < 0) { fprintf(stderr, "libusb_get_configuration failed: %s\n", libusb_error_name(ret)); goto out_libusb_close; } if (current_configuration != VISOMAT_CONFIGURATION) { ret = libusb_set_configuration(dev, VISOMAT_CONFIGURATION); if (ret < 0) { fprintf(stderr, "libusb_set_configuration failed: %s\n", libusb_error_name(ret)); fprintf(stderr, "Cannot set configuration %d\n", VISOMAT_CONFIGURATION); goto out_libusb_close; } } libusb_set_auto_detach_kernel_driver(dev, 1); ret = libusb_claim_interface(dev, VISOMAT_INTERFACE); if (ret < 0) { fprintf(stderr, "libusb_claim_interface failed: %s\n", libusb_error_name(ret)); fprintf(stderr, "Cannot claim interface %d\n", VISOMAT_INTERFACE); goto out_libusb_close; } /* Checking that the configuration has not changed, as suggested in * http://libusb.sourceforge.net/api-1.0/caveats.html */ current_configuration = -1; ret = libusb_get_configuration(dev, ¤t_configuration); if (ret < 0) { fprintf(stderr, "libusb_get_configuration after claim failed: %s\n", libusb_error_name(ret)); goto out_libusb_release_interface; } if (current_configuration != VISOMAT_CONFIGURATION) { fprintf(stderr, "libusb configuration changed (expected: %d, current: %d)\n", VISOMAT_CONFIGURATION, current_configuration); ret = -EINVAL; goto out_libusb_release_interface; } ret = visomat_get_datetime(dev); if (ret < 0) goto out_libusb_release_interface; ret = visomat_dump_eeprom(dev, 0x01 | 0x02); if (ret < 0) goto out_libusb_release_interface; out_libusb_release_interface: libusb_release_interface(dev, VISOMAT_INTERFACE); out_libusb_close: libusb_close(dev); dev = NULL; out_libusb_exit: libusb_exit(NULL); out: return ret; }