--- /dev/null
+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
--- /dev/null
+/*
+ * 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;
+}