From: Antonio Ospite Date: Wed, 13 Mar 2013 10:43:45 +0000 (+0100) Subject: Initial import X-Git-Url: https://git.ao2.it/visomat-utils.git/commitdiff_plain/ec6cf524733f65ff40ac2e086e96e0dfd38e1dda?ds=inline Initial import --- ec6cf524733f65ff40ac2e086e96e0dfd38e1dda diff --git a/README b/README new file mode 100644 index 0000000..2e14271 --- /dev/null +++ b/README @@ -0,0 +1,54 @@ +Utility software to access the Visomat Double Comfort blood pressure meter: +http://www.visomat.de/Oberarm-Blutdruckmes.372.0.html + +The device can store measurements for two distinct users and has a USB port +that can be used to download the history of measurements or trigger a new +measurement (apparently only for user 1). + +The device is seen by linux as a non-standard USB CDC device: + + [10217.072073] usb 2-5: new full-speed USB device number 5 using ohci_hcd + [10217.292067] usb 2-5: New USB device found, idVendor=1247, idProduct=00f8 + [10217.292079] usb 2-5: New USB device strings: Mfr=1, Product=2, SerialNumber=3 + [10217.292087] usb 2-5: Product: USB COMMUNICATION BRIDGE + [10217.292093] usb 2-5: Manufacturer: JAPAN PRECISION INSTRUMENTS + [10217.301191] cdc_acm 2-5:1.0: Zero length descriptor references + [10217.301212] cdc_acm: probe of 2-5:1.0 failed with error -22 + +This could be fixed adding a quirk to the cdc_acm driver in linux, but there +is not much of a point for this as the data is downloaded using USB bulk +communication anyways. + +A data packet starts with the STX control character, and ends with ETX. + +The payload is something like this: + + M106100101120211511021201001011208115610711610010112030103065073440601132001130680661001011311012708406610010112030106068066M20710010112031155111116100101120201170700764406011312012807107244060113170126050065440601131901140630661001011400011407007113031211450121080072 + +The structure can be better visualized when split it in fields: + + M # start of a Memory block + 1 # memory block index, corresponding to a "User ID" + 06 # number of records in this memory block + + # Each records stores date, time, systolic and diastolic pressure, pulses + + # yymmdd HHMM systolic diastolic pulses + 100101 1202 1151 102 120 + 100101 1208 1156 107 116 + 100101 1203 0103 065 073 + 440601 1320 0113 068 066 + 100101 1311 0127 084 066 + 100101 1203 0106 068 066 + + # Same structure for the other memory block + M + 2 + 07 + 100101 1203 1155 111 116 + 100101 1202 0117 070 076 + 440601 1312 0128 071 072 + 440601 1317 0126 050 065 + 440601 1319 0114 063 066 + 100101 1400 0114 070 071 + 130312 1145 0121 080 072 diff --git a/contrib/99-visomat.rules b/contrib/99-visomat.rules new file mode 100644 index 0000000..65653f8 --- /dev/null +++ b/contrib/99-visomat.rules @@ -0,0 +1,2 @@ +# Visomat Double Comfort blood pressure meter +ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="1247", ATTRS{idProduct}=="00f8", MODE="0660", GROUP="plugdev" diff --git a/contrib/lsusb-visomat-double-comfort.log b/contrib/lsusb-visomat-double-comfort.log new file mode 100644 index 0000000..b597e3a --- /dev/null +++ b/contrib/lsusb-visomat-double-comfort.log @@ -0,0 +1,102 @@ +Bus 002 Device 004: ID 1247:00f8 +Device Descriptor: + bLength 18 + bDescriptorType 1 + bcdUSB 1.10 + bDeviceClass 2 Communications + bDeviceSubClass 0 + bDeviceProtocol 0 + bMaxPacketSize0 64 + idVendor 0x1247 + idProduct 0x00f8 + bcdDevice 0.01 + iManufacturer 1 JAPAN PRECISION INSTRUMENTS + iProduct 2 USB COMMUNICATION BRIDGE + iSerial 3 + bNumConfigurations 1 + Configuration Descriptor: + bLength 9 + bDescriptorType 2 + wTotalLength 74 + bNumInterfaces 2 + bConfigurationValue 1 + iConfiguration 1 JAPAN PRECISION INSTRUMENTS + bmAttributes 0x80 + (Bus Powered) + MaxPower 30mA + Interface Descriptor: + bLength 9 + bDescriptorType 4 + bInterfaceNumber 0 + bAlternateSetting 0 + bNumEndpoints 2 + bInterfaceClass 2 Communications + bInterfaceSubClass 2 Abstract (modem) + bInterfaceProtocol 1 AT-commands (v.25ter) + iInterface 2 USB COMMUNICATION BRIDGE + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x81 EP 1 IN + bmAttributes 3 + Transfer Type Interrupt + Synch Type None + Usage Type Data + wMaxPacketSize 0x0010 1x 16 bytes + bInterval 100 + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x01 EP 1 OUT + bmAttributes 3 + Transfer Type Interrupt + Synch Type None + Usage Type Data + wMaxPacketSize 0x0010 1x 16 bytes + bInterval 255 + CDC Header: + bcdCDC 1.10 + CDC ACM: + bmCapabilities 0x06 + sends break + line coding and serial state + CDC Union: + bMasterInterface 0 + bSlaveInterface 1 + CDC Call Management: + bmCapabilities 0x03 + call management + use DataInterface + bDataInterface 1 + Interface Descriptor: + bLength 9 + bDescriptorType 4 + bInterfaceNumber 1 + bAlternateSetting 0 + bNumEndpoints 2 + bInterfaceClass 10 CDC Data + bInterfaceSubClass 0 Unused + bInterfaceProtocol 0 + iInterface 2 USB COMMUNICATION BRIDGE + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x82 EP 2 IN + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0040 1x 64 bytes + bInterval 0 + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x03 EP 3 OUT + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0040 1x 64 bytes + bInterval 0 +Device Status: 0x0000 + (Bus Powered) diff --git a/contrib/visomat.gnuplot b/contrib/visomat.gnuplot new file mode 100755 index 0000000..6ab72e6 --- /dev/null +++ b/contrib/visomat.gnuplot @@ -0,0 +1,12 @@ +#!/usr/bin/gnuplot -persist + +# TODO: deal with time of the day +set timefmt "%d/%m/%Y" +set xdata time + +set datafile separator ';' + +plot \ + 'data.csv' using 1:3 every ::1 with lp title "systolic", \ + 'data.csv' using 1:4 every ::1 with lp title "diastolic" , \ + 'data.csv' using 1:5 every ::1 with lp title "pulses" diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..371a75c --- /dev/null +++ b/src/Makefile @@ -0,0 +1,39 @@ +CFLAGS = -std=c99 -pedantic -pedantic-errors -Wall -g3 -O2 -D_ANSI_SOURCE_ +CFLAGS += -fno-common \ + -fvisibility=hidden \ + -Wall \ + -Wdeclaration-after-statement \ + -Wextra \ + -Wformat=2 \ + -Winit-self \ + -Winline \ + -Wpacked \ + -Wpointer-arith \ + -Wlarger-than-65500 \ + -Wmissing-declarations \ + -Wmissing-format-attribute \ + -Wmissing-noreturn \ + -Wmissing-prototypes \ + -Wnested-externs \ + -Wold-style-definition \ + -Wredundant-decls \ + -Wsign-compare \ + -Wstrict-aliasing=2 \ + -Wstrict-prototypes \ + -Wswitch-enum \ + -Wundef \ + -Wunreachable-code \ + -Wunsafe-loop-optimizations \ + -Wunused-but-set-variable \ + -Wwrite-strings \ + -Wp,-D_FORTIFY_SOURCE=2 \ + -fstack-protector \ + --param=ssp-buffer-size=4 + +CFLAGS += $(shell pkg-config --cflags libusb-1.0) +LDLIBS := $(shell pkg-config --libs libusb-1.0) + +visomat-data-downloader: visomat-data-downloader.o + +clean: + rm -f *~ *.o visomat-data-downloader diff --git a/src/visomat-data-downloader.c b/src/visomat-data-downloader.c new file mode 100644 index 0000000..0eca9c1 --- /dev/null +++ b/src/visomat-data-downloader.c @@ -0,0 +1,332 @@ +/* + * 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 + +#define STX 0x02 +#define ETX 0x03 + +#define DEVICE_VID 0x1247 +#define DEVICE_PID 0x00f8 + +#define EP_IN 0x82 +#define EP_OUT 0x03 + +#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 systolic; + unsigned int diastolic; + unsigned int pulses; +}; + +static inline int extract_datetime(unsigned char *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->pulses > 100) + printf("tachycardia"); + else if (p->pulses < 60) + printf("bradycardia"); + + printf("\n"); +} + +/* TODO separate better decoding data from printing it */ +static int decode_eeprom(unsigned char *buffer, unsigned int len) +{ + 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' || buffer[len - 1] != ETX) + 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; + + printf("# User: %d\n", user_id); + + 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), + "%04u%03u%03u", + &p.systolic, + &p.diastolic, + &p.pulses); + if (ret != 3) + return -EINVAL; + + /* pressure data is 10 bytes */ + i += 10; + + print_record_csv_compat(&d, &p); + } + } + } + + return 0; +} + +static int decode_datetime(unsigned char *buffer, unsigned int len) +{ + int ret; + unsigned char code[4] = { 0 }; + struct datetime d; + unsigned char *pbuffer = buffer; + + if (len != 15) + return -EINVAL; + + code[0] = buffer[1]; + code[1] = buffer[2]; + code[2] = buffer[3]; + + ret = extract_datetime(pbuffer + 4, &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; + unsigned char 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, 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, + unsigned char *buffer, + unsigned int len) +{ + int ret; + int transferred; + unsigned char response[64] = { 0 }; + unsigned int i; + + i = 0; + do { + unsigned int j; + + transferred = 0; + ret = libusb_bulk_transfer(dev, 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 the buffer is 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) +{ + /* Assuming an EEPROM of 1 KiB */ + unsigned char buffer[1024] = { 0 }; + int ret; + + ret = send_command(dev, VISOMAT_CMD_DUMP_EEPROM); + if (ret < 0) + return ret; + + ret = get_response(dev, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + ret = decode_eeprom(buffer, ret); + if (ret < 0) + return ret; + + return 0; +} + +static int visomat_get_datetime(visomat_device *dev) +{ + unsigned char 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; +} + +int main(void) +{ + int ret; + libusb_device_handle *dev; + + libusb_init(NULL); + libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_INFO); + + dev = libusb_open_device_with_vid_pid(NULL, DEVICE_VID, DEVICE_PID); + if (dev == NULL) { + fprintf(stderr, "Couldn't open device.\n"); + ret = -ENODEV; + goto out_libusb_exit; + } + + libusb_set_configuration(dev, 1); + libusb_claim_interface(dev, 1); + libusb_detach_kernel_driver(dev, 1); + + ret = visomat_get_datetime(dev); + if (ret < 0) + goto out; + + ret = visomat_dump_eeprom(dev); + +out: + libusb_close(dev); +out_libusb_exit: + libusb_exit(NULL); + return ret; +}