Initial import
authorAntonio Ospite <ospite@studenti.unina.it>
Wed, 13 Mar 2013 10:43:45 +0000 (11:43 +0100)
committerAntonio Ospite <ospite@studenti.unina.it>
Wed, 13 Mar 2013 10:48:35 +0000 (11:48 +0100)
README [new file with mode: 0644]
contrib/99-visomat.rules [new file with mode: 0644]
contrib/lsusb-visomat-double-comfort.log [new file with mode: 0644]
contrib/visomat.gnuplot [new file with mode: 0755]
src/Makefile [new file with mode: 0644]
src/visomat-data-downloader.c [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
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 (file)
index 0000000..65653f8
--- /dev/null
@@ -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 (file)
index 0000000..b597e3a
--- /dev/null
@@ -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 (executable)
index 0000000..6ab72e6
--- /dev/null
@@ -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 (file)
index 0000000..371a75c
--- /dev/null
@@ -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 (file)
index 0000000..0eca9c1
--- /dev/null
@@ -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 <ospite@studenti.unina.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <libusb.h>
+
+#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;
+}