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