README: mention that the Prevention DS-2200PV uses the same protocol
[visomat-utils.git] / src / visomat-data-downloader.c
1 /*
2  * visomat-data-downloader - download data from Visomat Double Comfort
3  *
4  * The Visomat Double Comfort is a blood pressure meter with a USB port.
5  *
6  * Copyright (C) 2013  Antonio Ospite <ospite@studenti.unina.it>
7  *
8  * This program is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 #include <stdio.h>
23 #include <stdbool.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <errno.h>
27 #include <unistd.h>
28 #include <libusb.h>
29
30 #ifdef DEBUG
31 #define debug(...) fprintf(stderr, __VA_ARGS__)
32 static void debug_dump_buffer(const char *filename, uint8_t *buffer, unsigned int len)
33 {
34         FILE *dump_file;
35
36         dump_file = fopen(filename, "wb");
37         if (dump_file == NULL) {
38                 fprintf(stderr, "Failed to open %s: %s\n", filename, strerror(errno));
39                 return;
40         }
41
42         fwrite(buffer, 1, len, dump_file);
43         fclose(dump_file);
44 }
45 #else
46 #define debug(...) do {} while(0)
47 static void debug_dump_buffer(const char *filename, uint8_t *buffer, unsigned int len)
48 {
49         (void)filename;
50         (void)buffer;
51         (void)len;
52 }
53 #endif
54
55 #define VISOMAT_DEVICE_VID    0x1247
56 #define VISOMAT_DEVICE_PID    0x00f8
57 #define VISOMAT_CONFIGURATION 1
58 #define VISOMAT_INTERFACE     1
59 #define VISOMAT_EP_IN         0x82
60 #define VISOMAT_EP_OUT        0x03
61 #define VISOMAT_PACKET_SIZE   64
62
63 #define STX 0x02
64 #define ETX 0x03
65
66 #define DATETIME_PACKET_SIZE 15
67 #define BASE_YEAR 2000
68
69 typedef enum {
70         VISOMAT_CMD_DUMP_EEPROM,
71         VISOMAT_CMD_UNKNOWN1, /* maybe the firmware revision */
72         VISOMAT_CMD_GET_DATETIME,
73         VISOMAT_CMD_START_MEASURE1,
74         VISOMAT_CMD_UNKNOWN2, /* XXX transmission hangs */
75         VISOMAT_CMD_UNKNOWN3,
76 } visomat_command;
77
78 static char command_codes[][3] = {
79         [VISOMAT_CMD_DUMP_EEPROM]    = "R00",
80         [VISOMAT_CMD_UNKNOWN1]       = "R01",
81         [VISOMAT_CMD_GET_DATETIME]   = "R02",
82         [VISOMAT_CMD_START_MEASURE1] = "R03",
83         [VISOMAT_CMD_UNKNOWN2]       = "R04",
84         [VISOMAT_CMD_UNKNOWN3]       = "R05",
85 };
86
87 struct datetime  {
88         unsigned int year;
89         unsigned int month;
90         unsigned int day;
91         unsigned int hour;
92         unsigned int minute;
93 };
94
95 struct pressure  {
96         unsigned int flag; /* XXX Maybe this means arrhythmia? */
97         unsigned int systolic;
98         unsigned int diastolic;
99         unsigned int pulses;
100 };
101
102 static inline int extract_datetime(uint8_t *buffer, struct datetime *d)
103 {
104         int ret;
105
106         ret = sscanf((char *)buffer, "%02u%02u%02u%02u%02u",
107                      &d->year,
108                      &d->month,
109                      &d->day,
110                      &d->hour,
111                      &d->minute);
112         if (ret != 5)
113                 return -EINVAL;
114
115         d->year += BASE_YEAR;
116
117         return 0;
118 }
119
120 /* 
121  * NOTE: the CSV output is meant to be compatible with one from the Windows
122  * program, which uses the %d/%m/%Y date format, so that's not my fault.
123  */
124 static void print_record_csv_compat(struct datetime *d, struct pressure *p)
125 {
126         unsigned int pulse_pressure;
127
128         printf("%02u/%02u/%04u;%02u.%02u;",
129                d->day, d->month, d->year, d->hour, d->minute);
130
131         printf("%u;%u;%u;", p->systolic, p->diastolic, p->pulses);
132
133         pulse_pressure = p->systolic - p->diastolic;
134         printf("%u;", pulse_pressure);
135
136         if (p->flag)
137                 printf("x");
138
139 #if 0
140         /* The original software does not seem to be doing that */
141         if (p->pulses > 100)
142                 printf("tachycardia");
143         else if (p->pulses < 60)
144                 printf("bradycardia");
145 #endif
146
147         printf("\n");
148 }
149
150 /* TODO: it would be better to separate decoding data from printing it */
151 static int decode_eeprom(uint8_t *buffer,
152                          unsigned int len,
153                          unsigned int user_mask)
154 {
155         int ret;
156         unsigned int n;
157         unsigned int i;
158         unsigned int j;
159         unsigned int user_id;
160         unsigned int num_records;
161         struct datetime d;
162         struct pressure p;
163
164         if (buffer[0] != STX || buffer[1] != 'M') {
165                 fprintf(stderr, "Usupported data.\n");
166                 return -EINVAL;
167         }
168
169         if (buffer[len - 1] != ETX) {
170                 fprintf(stderr, "Bad terminator in data. Buffer too small?\n");
171                 return -EINVAL;
172         }
173
174         /* skip the initial STX */
175         i = 1;
176
177         /* and the final ETX */
178         n = len - 1;
179
180         while (i < n) {
181                 /* Each user record begins with 'M', maybe for "Memory" */
182                 if (buffer[i] == 'M') {
183                         /* i tracks the bytes consumed */
184                         i += 1;
185
186                         ret = sscanf((char *)(buffer + i), "%1u%02u",
187                                      &user_id, &num_records);
188                         if (ret != 2)
189                                 return -EINVAL;
190
191                         /* user_id and num_records take 3 bytes */
192                         i += 3;
193
194                         /*
195                          * when there are no records, there is a dummy byte
196                          * which has to be consumed
197                          */
198                         if (num_records == 0)
199                                 i += 1;
200
201                         for (j = 0; j < num_records; j++) {
202                                 ret = extract_datetime(buffer + i, &d);
203                                 if (ret < 0)
204                                         return ret;
205
206                                 /* datetime takes 10 bytes */
207                                 i += 10;
208
209                                 ret = sscanf((char *)(buffer + i),
210                                              "%1u%03u%03u%03u",
211                                              &p.flag,
212                                              &p.systolic,
213                                              &p.diastolic,
214                                              &p.pulses);
215                                 if (ret != 4)
216                                         return -EINVAL;
217
218                                 /* pressure data is 10 bytes */
219                                 i += 10;
220
221                                 /* TODO: split out the printing part */
222                                 if (user_id & user_mask) {
223                                         if (j == 0)
224                                                 printf("# User: %d\n", user_id);
225                                         print_record_csv_compat(&d, &p);
226                                 }
227                         }
228                 }
229         }
230
231         return 0;
232 }
233
234 static int decode_datetime(uint8_t *buffer, unsigned int len)
235 {
236         int ret;
237         uint8_t code[4] = { 0 }; /* the initial STX + 3 bytes command code */
238         struct datetime d;
239         uint8_t *pbuffer = buffer;
240
241         if (len != DATETIME_PACKET_SIZE)
242                 return -EINVAL;
243
244         code[0] = buffer[1];
245         code[1] = buffer[2];
246         code[2] = buffer[3];
247
248         ret = extract_datetime(pbuffer + sizeof(code), &d);
249         if (ret < 0)
250                 return ret;
251
252         printf("# (%s) Date: %04d/%02d/%02d %02d:%02d\n",
253                code, d.year, d.month, d.day, d.hour, d.minute);
254
255         return 0;
256 }
257
258 static int send_command(libusb_device_handle *dev, visomat_command cmd)
259 {
260         int ret;
261         int transferred;
262         uint8_t request[5];
263
264         request[0] = STX;
265         request[1] = command_codes[cmd][0];
266         request[2] = command_codes[cmd][1];
267         request[3] = command_codes[cmd][2];
268         request[4] = ETX;
269
270         transferred = 0;
271         ret = libusb_bulk_transfer(dev, VISOMAT_EP_OUT, request, sizeof(request), &transferred, 0);
272         if (ret != 0 || transferred != sizeof(request)) {
273                 fprintf(stderr, "Error: sending request: %d (%s)\ttransferred: %d (expected %zu)\n",
274                         ret, libusb_error_name(ret), transferred, sizeof(request));
275                 return ret;
276         }
277         return 0;
278 }
279
280 static int get_response(libusb_device_handle *dev,
281                         uint8_t *buffer,
282                         unsigned int len)
283 {
284         int ret;
285         int transferred;
286         uint8_t response[VISOMAT_PACKET_SIZE] = { 0 };
287         unsigned int i;
288
289         i = 0;
290         do {
291                 unsigned int j;
292
293                 transferred = 0;
294                 ret = libusb_bulk_transfer(dev, VISOMAT_EP_IN, response, sizeof(response), &transferred, 5000);
295                 if (ret != 0) {
296                         fprintf(stderr, "Error getting response: %d (%s)\ttransferred: %d (expected %zu)\n",
297                                 ret, libusb_error_name(ret), transferred, sizeof(response));
298                         return ret;
299                 }
300
301                 for (j = 0; j < (unsigned int)transferred; j++) {
302                         buffer[i] = response[j];
303                         i += 1;
304                 }
305
306         } while (buffer[i - 1] != ETX && i < len);
307
308         /* Check if the buffer contains a valid response packet */
309         if (buffer[0] != STX || buffer[i - 1] != ETX)
310                 return -EINVAL;
311
312         return i;
313 }
314
315 /* Candidates for a future public API, if a shared library will ever be made */
316 #define visomat_device libusb_device_handle
317 static int visomat_dump_eeprom(visomat_device *dev, unsigned int user_mask)
318 {
319         /* Assuming an EEPROM of 4 KiB  */
320         uint8_t buffer[4096] = { 0 };
321         int ret;
322
323         ret = send_command(dev, VISOMAT_CMD_DUMP_EEPROM);
324         if (ret < 0)
325                 return ret;
326
327         ret = get_response(dev, buffer, sizeof(buffer));
328         debug("buffer size: %d\n", ret);
329         debug_dump_buffer("eeprom.bin", buffer, sizeof(buffer));
330         if (ret < 0)
331                 return ret;
332
333         ret = decode_eeprom(buffer, ret, user_mask);
334         if (ret < 0)
335                 return ret;
336
337         return 0;
338 }
339
340 static int visomat_get_datetime(visomat_device *dev)
341 {
342         uint8_t buffer[255] = { 0 };
343         int ret;
344
345         ret = send_command(dev, VISOMAT_CMD_GET_DATETIME);
346         if (ret < 0)
347                 return ret;
348
349         ret = get_response(dev, buffer, sizeof(buffer));
350         if (ret < 0)
351                 return ret;
352
353         ret = decode_datetime(buffer, ret);
354         if (ret < 0)
355                 return ret;
356
357         return 0;
358 }
359
360 static void usage(const char *name)
361 {
362         printf("usage: %s [OPTIONS]\n\n", name);
363         printf("OPTIONS:\n");
364         printf("\t-D\t\tenable libusb debug output\n");
365         printf("\t-h\t\tthis help message\n");
366 }
367
368 int main(int argc, char *argv[])
369 {
370         int ret;
371         int opt;
372         bool enable_libusb_debug = false;
373         libusb_device_handle *dev;
374         int current_configuration;
375
376         while ((opt = getopt(argc, argv, "Dh")) != -1) {
377                 switch (opt) {
378                 case 'D':
379                         enable_libusb_debug = true;
380                         break;
381                 case 'h':
382                         usage(argv[0]);
383                         ret = 0;
384                         goto out;
385                 default: /* '?' */
386                         usage(argv[0]);
387                         ret = -EINVAL;
388                         goto out;
389                 }
390         }
391
392         ret = libusb_init(NULL);
393         if (ret < 0) {
394                 fprintf(stderr, "libusb_init failed: %s\n",
395                         libusb_error_name(ret));
396                 goto out;
397         }
398
399         libusb_set_debug(NULL, enable_libusb_debug ?
400                          LIBUSB_LOG_LEVEL_DEBUG : LIBUSB_LOG_LEVEL_INFO);
401
402         dev = libusb_open_device_with_vid_pid(NULL,
403                                               VISOMAT_DEVICE_VID,
404                                               VISOMAT_DEVICE_PID);
405         if (dev == NULL) {
406                 fprintf(stderr, "libusb_open failed: %s\n", strerror(errno));
407                 ret = -errno;
408                 goto out_libusb_exit;
409         }
410
411         current_configuration = -1;
412         ret = libusb_get_configuration(dev, &current_configuration);
413         if (ret < 0) {
414                 fprintf(stderr, "libusb_get_configuration failed: %s\n",
415                         libusb_error_name(ret));
416                 goto out_libusb_close;
417         }
418
419         if (current_configuration != VISOMAT_CONFIGURATION) {
420                 ret = libusb_set_configuration(dev, VISOMAT_CONFIGURATION);
421                 if (ret < 0) {
422                         fprintf(stderr, "libusb_set_configuration failed: %s\n",
423                                 libusb_error_name(ret));
424                         fprintf(stderr, "Cannot set configuration %d\n",
425                                 VISOMAT_CONFIGURATION);
426                         goto out_libusb_close;
427                 }
428         }
429
430         libusb_set_auto_detach_kernel_driver(dev, 1);
431
432         ret = libusb_claim_interface(dev, VISOMAT_INTERFACE);
433         if (ret < 0) {
434                 fprintf(stderr, "libusb_claim_interface failed: %s\n",
435                         libusb_error_name(ret));
436                 fprintf(stderr, "Cannot claim interface %d\n",
437                         VISOMAT_INTERFACE);
438                 goto out_libusb_close;
439         }
440
441         /* Checking that the configuration has not changed, as suggested in
442          * http://libusb.sourceforge.net/api-1.0/caveats.html
443          */
444         current_configuration = -1;
445         ret = libusb_get_configuration(dev, &current_configuration);
446         if (ret < 0) {
447                 fprintf(stderr, "libusb_get_configuration after claim failed: %s\n",
448                         libusb_error_name(ret));
449                 goto out_libusb_release_interface;
450         }
451
452         if (current_configuration != VISOMAT_CONFIGURATION) {
453                 fprintf(stderr, "libusb configuration changed (expected: %d, current: %d)\n",
454                         VISOMAT_CONFIGURATION, current_configuration);
455                 ret = -EINVAL;
456                 goto out_libusb_release_interface;
457         }
458
459         ret = visomat_get_datetime(dev);
460         if (ret < 0)
461                 goto out_libusb_release_interface;
462
463         ret = visomat_dump_eeprom(dev, 0x01 | 0x02);
464         if (ret < 0)
465                 goto out_libusb_release_interface;
466
467 out_libusb_release_interface:
468         libusb_release_interface(dev, VISOMAT_INTERFACE);
469 out_libusb_close:
470         libusb_close(dev);
471         dev = NULL;
472 out_libusb_exit:
473         libusb_exit(NULL);
474 out:
475         return ret;
476 }