Add a way to filter measurements by user ids
[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 <errno.h>
25 #include <unistd.h>
26 #include <libusb.h>
27
28 #define STX 0x02
29 #define ETX 0x03
30
31 #define DEVICE_VID 0x1247
32 #define DEVICE_PID 0x00f8
33
34 #define EP_IN 0x82
35 #define EP_OUT 0x03
36
37 #define BASE_YEAR 2000
38
39 typedef enum {
40         VISOMAT_CMD_DUMP_EEPROM,
41         VISOMAT_CMD_UNKNOWN1, /* maybe the firmware revision */
42         VISOMAT_CMD_GET_DATETIME,
43         VISOMAT_CMD_START_MEASURE1,
44         VISOMAT_CMD_UNKNOWN2, /* XXX transmission hangs */
45         VISOMAT_CMD_UNKNOWN3,
46 } visomat_command;
47
48 static char command_codes[][3] = {
49         [VISOMAT_CMD_DUMP_EEPROM]    = "R00",
50         [VISOMAT_CMD_UNKNOWN1]       = "R01",
51         [VISOMAT_CMD_GET_DATETIME]   = "R02",
52         [VISOMAT_CMD_START_MEASURE1] = "R03",
53         [VISOMAT_CMD_UNKNOWN2]       = "R04",
54         [VISOMAT_CMD_UNKNOWN3]       = "R05",
55 };
56
57 struct datetime  {
58         unsigned int year;
59         unsigned int month;
60         unsigned int day;
61         unsigned int hour;
62         unsigned int minute;
63 };
64
65 struct pressure  {
66         unsigned int flag; /* XXX Maybe this means arrhythmia? */
67         unsigned int systolic;
68         unsigned int diastolic;
69         unsigned int pulses;
70 };
71
72 static inline int extract_datetime(unsigned char *buffer, struct datetime *d)
73 {
74         int ret;
75
76         ret = sscanf((char *)buffer, "%02u%02u%02u%02u%02u",
77                      &d->year,
78                      &d->month,
79                      &d->day,
80                      &d->hour,
81                      &d->minute);
82         if (ret != 5)
83                 return -EINVAL;
84
85         d->year += BASE_YEAR;
86
87         return 0;
88 }
89
90 /* 
91  * NOTE: the CSV output is meant to be compatible with one from the Windows
92  * program, which uses the %d/%m/%Y date format, so that's not my fault.
93  */
94 static void print_record_csv_compat(struct datetime *d, struct pressure *p)
95 {
96         unsigned int pulse_pressure;
97
98         printf("%02u/%02u/%04u;%02u.%02u;",
99                d->day, d->month, d->year, d->hour, d->minute);
100
101         printf("%u;%u;%u;", p->systolic, p->diastolic, p->pulses);
102
103         pulse_pressure = p->systolic - p->diastolic;
104         printf("%u;", pulse_pressure);
105
106         if (p->flag)
107                 printf("x");
108
109 #if 0
110         /* The original software does not seem to be doing that */
111         if (p->pulses > 100)
112                 printf("tachycardia");
113         else if (p->pulses < 60)
114                 printf("bradycardia");
115 #endif
116
117         printf("\n");
118 }
119
120 /* TODO separate better decoding data from printing it */
121 static int decode_eeprom(unsigned char *buffer,
122                          unsigned int len,
123                          unsigned int user_mask)
124 {
125         int ret;
126         unsigned int n;
127         unsigned int i;
128         unsigned int j;
129         unsigned int user_id;
130         unsigned int num_records;
131         struct datetime d;
132         struct pressure p;
133
134         if (buffer[0] != STX || buffer[1] != 'M' || buffer[len - 1] != ETX)
135                 return -EINVAL;
136
137         /* skip the initial STX */
138         i = 1;
139
140         /* and the final ETX */
141         n = len - 1;
142
143         while (i < n) {
144                 /* Each user record begins with 'M', maybe for "Memory" */
145                 if (buffer[i] == 'M') {
146                         /* i tracks the bytes consumed */
147                         i += 1;
148
149                         ret = sscanf((char *)(buffer + i), "%1u%02u",
150                                      &user_id, &num_records);
151                         if (ret != 2)
152                                 return -EINVAL;
153
154                         /* user_id and num_records take 3 bytes */
155                         i += 3;
156
157                         for (j = 0; j < num_records; j++) {
158                                 ret = extract_datetime(buffer + i, &d);
159                                 if (ret < 0)
160                                         return ret;
161
162                                 /* datetime takes 10 bytes */
163                                 i += 10;
164
165                                 ret = sscanf((char *)(buffer + i),
166                                              "%1u%03u%03u%03u",
167                                              &p.flag,
168                                              &p.systolic,
169                                              &p.diastolic,
170                                              &p.pulses);
171                                 if (ret != 4)
172                                         return -EINVAL;
173
174                                 /* pressure data is 10 bytes */
175                                 i += 10;
176
177                                 /* TODO: split out the printing part */
178                                 if (user_id & user_mask) {
179                                         if (j == 0)
180                                                 printf("# User: %d\n", user_id);
181                                         print_record_csv_compat(&d, &p);
182                                 }
183                         }
184                 }
185         }
186
187         return 0;
188 }
189
190 static int decode_datetime(unsigned char *buffer, unsigned int len)
191 {
192         int ret;
193         unsigned char code[4] = { 0 };
194         struct datetime d;
195         unsigned char *pbuffer = buffer;
196
197         if (len != 15)
198                 return -EINVAL;
199
200         code[0] = buffer[1];
201         code[1] = buffer[2];
202         code[2] = buffer[3];
203
204         ret = extract_datetime(pbuffer + 4, &d);
205         if (ret < 0)
206                 return ret;
207
208         printf("# (%s) Date: %04d/%02d/%02d %02d:%02d\n",
209                code, d.year, d.month, d.day, d.hour, d.minute);
210
211         return 0;
212 }
213
214 static int send_command(libusb_device_handle *dev, visomat_command cmd)
215 {
216         int ret;
217         int transferred;
218         unsigned char request[5];
219
220         request[0] = STX;
221         request[1] = command_codes[cmd][0];
222         request[2] = command_codes[cmd][1];
223         request[3] = command_codes[cmd][2];
224         request[4] = ETX;
225
226         transferred = 0;
227         ret = libusb_bulk_transfer(dev, EP_OUT, request, sizeof(request), &transferred, 0);
228         if (ret != 0 || transferred != sizeof(request)) {
229                 fprintf(stderr, "Error: sending request: %d (%s)\ttransferred: %d (expected %zu)\n",
230                         ret, libusb_error_name(ret), transferred, sizeof(request));
231                 return ret;
232         }
233         return 0;
234 }
235
236 static int get_response(libusb_device_handle *dev,
237                         unsigned char *buffer,
238                         unsigned int len)
239 {
240         int ret;
241         int transferred;
242         unsigned char response[64] = { 0 };
243         unsigned int i;
244
245         i = 0;
246         do {
247                 unsigned int j;
248
249                 transferred = 0;
250                 ret = libusb_bulk_transfer(dev, EP_IN, response, sizeof(response), &transferred, 5000);
251                 if (ret != 0) {
252                         fprintf(stderr, "Error getting response: %d (%s)\ttransferred: %d (expected %zu)\n",
253                                 ret, libusb_error_name(ret), transferred, sizeof(response));
254                         return ret;
255                 }
256
257                 for (j = 0; j < (unsigned int)transferred; j++) {
258                         buffer[i] = response[j];
259                         i += 1;
260                 }
261
262         } while (buffer[i - 1] != ETX && i < len);
263
264         /* Check the buffer is a valid response packet */
265         if (buffer[0] != STX || buffer[i - 1] != ETX)
266                 return -EINVAL;
267
268         return i;
269 }
270
271 /* Candidates for a future public API, if a shared library will ever be made */
272 #define visomat_device libusb_device_handle
273 static int visomat_dump_eeprom(visomat_device *dev, unsigned int user_mask)
274 {
275         /* Assuming an EEPROM of 1 KiB  */
276         unsigned char buffer[1024] = { 0 };
277         int ret;
278
279         ret = send_command(dev, VISOMAT_CMD_DUMP_EEPROM);
280         if (ret < 0)
281                 return ret;
282
283         ret = get_response(dev, buffer, sizeof(buffer));
284         if (ret < 0)
285                 return ret;
286
287         ret = decode_eeprom(buffer, ret, user_mask);
288         if (ret < 0)
289                 return ret;
290
291         return 0;
292 }
293
294 static int visomat_get_datetime(visomat_device *dev)
295 {
296         unsigned char buffer[255] = { 0 };
297         int ret;
298
299         ret = send_command(dev, VISOMAT_CMD_GET_DATETIME);
300         if (ret < 0)
301                 return ret;
302
303         ret = get_response(dev, buffer, sizeof(buffer));
304         if (ret < 0)
305                 return ret;
306
307         ret = decode_datetime(buffer, ret);
308         if (ret < 0)
309                 return ret;
310
311         return 0;
312 }
313
314 int main(void)
315 {
316         int ret;
317         libusb_device_handle *dev;
318
319         libusb_init(NULL);
320         libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_INFO);
321
322         dev = libusb_open_device_with_vid_pid(NULL, DEVICE_VID, DEVICE_PID);
323         if (dev == NULL) {
324                 fprintf(stderr, "Couldn't open device.\n");
325                 ret = -ENODEV;
326                 goto out_libusb_exit;
327         }
328
329         libusb_set_configuration(dev, 1);
330         libusb_detach_kernel_driver(dev, 1);
331         libusb_claim_interface(dev, 1);
332
333         ret = visomat_get_datetime(dev);
334         if (ret < 0)
335                 goto out;
336
337         ret = visomat_dump_eeprom(dev, 0x01 | 0x02);
338
339 out:
340         libusb_close(dev);
341 out_libusb_exit:
342         libusb_exit(NULL);
343         return ret;
344 }