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