]> sigrok.org Git - libsigrok.git/blob - src/dmm/bm86x.c
output/csv: use intermediate time_t var, silence compiler warning
[libsigrok.git] / src / dmm / bm86x.c
1 /*
2  * This file is part of the libsigrok project.
3  *
4  * Copyright (C) 2014 Aurelien Jacobs <aurel@gnuage.org>
5  * Copyright (C) 2019 Gerhard Sittig <gerhard.sittig@gmx.net>
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 /**
22  * @file
23  *
24  * Brymen BM86x serial protocol parser. The USB protocol (for the cable)
25  * and the packet description (for the meter) were retrieved from:
26  * http://www.brymen.com/Download2.html
27  * http://www.brymen.com/PD02BM860s_protocolDL.html
28  * http://www.brymen.com/images/DownloadList/ProtocolList/BM860-BM860s_List/BM860-BM860s-500000-count-dual-display-DMMs-protocol.pdf
29  * http://web.archive.org/web/20191231053213/http://www.brymen.com/images/DownloadList/ProtocolList/BM860-BM860s_List/BM860-BM860s-500000-count-dual-display-DMMs-protocol.pdf
30  */
31
32 #include <config.h>
33 #include <math.h>
34 #include <libsigrok/libsigrok.h>
35 #include "libsigrok-internal.h"
36 #include <string.h>
37
38 #define LOG_PREFIX "brymen-bm86x"
39
40 #ifdef HAVE_SERIAL_COMM
41 SR_PRIV int sr_brymen_bm86x_packet_request(struct sr_serial_dev_inst *serial)
42 {
43         static const uint8_t request[] = { 0x00, 0x00, 0x86, 0x66, };
44
45         serial_write_nonblocking(serial, request, sizeof(request));
46
47         return SR_OK;
48 }
49 #endif
50
51 SR_PRIV gboolean sr_brymen_bm86x_packet_valid(const uint8_t *buf)
52 {
53         /*
54          * "Model ID3" (3rd HID report, byte 3) is the only documented
55          * fixed value, and must be 0x86. All other positions either depend
56          * on the meter's function, or the measurement's value, or are not
57          * documented by the vendor (are marked as "don't care", no fixed
58          * values are listed). There is nothing else we can check reliably.
59          */
60         if (buf[19] != 0x86)
61                 return FALSE;
62
63         /*
64          * 2021-05 update: The devices that we have seen in the field do
65          * provide four bytes which we can synchronize to. Which happens
66          * to match the bm52x and bm82x devices' protocol. This condition
67          * is not documented by the vendor, but improves reliability of
68          * the re-synchronization for slight offsets, which were seen
69          * in the field, and which would have kept failing in an earlier
70          * implementation.
71          */
72         if (buf[16] != 0x86)
73                 return FALSE;
74         if (buf[17] != 0x86)
75                 return FALSE;
76         if (buf[18] != 0x86)
77                 return FALSE;
78
79         return TRUE;
80 }
81
82 /*
83  * Data bytes in the DMM packet encode LCD segments in an unusual order
84  * (bgcdafe) and in an unusual position (bits 7:1 within the byte). The
85  * decimal point (bit 0) for one digit resides in the _next_ digit's byte.
86  *
87  * These routines convert LCD segments to characters, and a section of the
88  * DMM packet (which corresponds to the primary or secondary display) to
89  * the text representation of the measurement's value, before regular text
90  * to number conversion is applied. The first byte of the passed in block
91  * contains indicators, the value's digits start at the second byte.
92  */
93
94 static char brymen_bm86x_parse_digit(uint8_t b)
95 {
96         switch (b >> 1) {
97         /* Sign. */
98         case 0x20: return '-';
99         /* Decimal digits. */
100         case 0x5f: return '0';
101         case 0x50: return '1';
102         case 0x6d: return '2';
103         case 0x7c: return '3';
104         case 0x72: return '4';
105         case 0x3e: return '5';
106         case 0x3f: return '6';
107         case 0x54: return '7';
108         case 0x7f: return '8';
109         case 0x7e: return '9';
110         /* Temperature units. */
111         case 0x0f: return 'C';
112         case 0x27: return 'F';
113         /* OL condition, and diode mode. */
114         case 0x0b: return 'L';
115         case 0x79: return 'd';
116         case 0x10: return 'i';
117         case 0x39: return 'o';
118         /* Blank digit. */
119         case 0x00: return '\0';
120         /* Invalid or unknown segment combination. */
121         default:
122                 sr_warn("Unknown encoding for digit: 0x%02x.", b);
123                 return '\0';
124         }
125 }
126
127 static int brymen_bm86x_parse_digits(const uint8_t *pkt, size_t pktlen,
128         char *txtbuf, float *value, char *temp_unit, int *digits, int signflag)
129 {
130         uint8_t byte;
131         char *txtptr, txtchar;
132         size_t pos;
133         int ret;
134
135         txtptr = txtbuf;
136         if (digits)
137                 *digits = INT_MIN;
138
139         if (pkt[0] & signflag)
140                 *txtptr++ = '-';
141         for (pos = 0; pos < pktlen; pos++) {
142                 byte = pkt[1 + pos];
143                 if (pos && pos < 5 && (byte & 0x01)) {
144                         *txtptr++ = '.';
145                         if (digits)
146                                 *digits = 0;
147                 }
148                 txtchar = brymen_bm86x_parse_digit(byte);
149                 if (pos == 5 && (txtchar == 'C' || txtchar == 'F')) {
150                         if (temp_unit)
151                                 *temp_unit = txtchar;
152                 } else if (txtchar) {
153                         *txtptr++ = txtchar;
154                         if (digits)
155                                 (*digits)++;
156                 }
157         }
158         *txtptr = '\0';
159
160         if (digits && *digits < 0)
161                 *digits = 0;
162
163         ret = value ? sr_atof_ascii(txtbuf, value) : SR_OK;
164         if (ret != SR_OK) {
165                 sr_dbg("invalid float string: '%s'", txtbuf);
166                 return ret;
167         }
168
169         return SR_OK;
170 }
171
172 /*
173  * Extract the measurement value and its properties for one of the
174  * meter's displays from the DMM packet.
175  */
176 static void brymen_bm86x_parse(const uint8_t *buf, float *floatval,
177         struct sr_datafeed_analog *analog, size_t ch_idx)
178 {
179         char txtbuf[16], temp_unit;
180         int ret, digits, is_diode, over_limit, scale;
181         uint8_t ind1, ind15;
182
183         temp_unit = '\0';
184         if (ch_idx == 0) {
185                 /*
186                  * Main display. Note that _some_ of the second display's
187                  * indicators are involved in the inspection of the _first_
188                  * display's measurement value. So we have to get the
189                  * second display's text buffer here, too.
190                  */
191                 (void)brymen_bm86x_parse_digits(&buf[9], 4, txtbuf,
192                         NULL, NULL, NULL, 0);
193                 is_diode = strcmp(txtbuf, "diod") == 0;
194                 ret = brymen_bm86x_parse_digits(&buf[2], 6, txtbuf,
195                         floatval, &temp_unit, &digits, 0x80);
196                 over_limit = strstr(txtbuf, "0L") || strstr(txtbuf, "0.L");
197                 if (ret != SR_OK && !over_limit)
198                         return;
199
200                 /* SI unit. */
201                 if (buf[8] & 0x01) {
202                         analog->meaning->mq = SR_MQ_VOLTAGE;
203                         analog->meaning->unit = SR_UNIT_VOLT;
204                         if (is_diode) {
205                                 analog->meaning->mqflags |= SR_MQFLAG_DIODE;
206                                 analog->meaning->mqflags |= SR_MQFLAG_DC;
207                         }
208                 } else if (buf[14] & 0x80) {
209                         analog->meaning->mq = SR_MQ_CURRENT;
210                         analog->meaning->unit = SR_UNIT_AMPERE;
211                 } else if (buf[14] & 0x20) {
212                         analog->meaning->mq = SR_MQ_CAPACITANCE;
213                         analog->meaning->unit = SR_UNIT_FARAD;
214                 } else if (buf[14] & 0x10) {
215                         analog->meaning->mq = SR_MQ_CONDUCTANCE;
216                         analog->meaning->unit = SR_UNIT_SIEMENS;
217                 } else if (buf[15] & 0x01) {
218                         analog->meaning->mq = SR_MQ_FREQUENCY;
219                         analog->meaning->unit = SR_UNIT_HERTZ;
220                 } else if (buf[10] & 0x01) {
221                         analog->meaning->mq = SR_MQ_CONTINUITY;
222                         analog->meaning->unit = SR_UNIT_OHM;
223                 } else if (buf[15] & 0x10) {
224                         analog->meaning->mq = SR_MQ_RESISTANCE;
225                         analog->meaning->unit = SR_UNIT_OHM;
226                 } else if (buf[15] & 0x02) {
227                         analog->meaning->mq = SR_MQ_POWER;
228                         analog->meaning->unit = SR_UNIT_DECIBEL_MW;
229                 } else if (buf[15] & 0x80) {
230                         analog->meaning->mq = SR_MQ_DUTY_CYCLE;
231                         analog->meaning->unit = SR_UNIT_PERCENTAGE;
232                 } else if ((buf[2] & 0x0a) && temp_unit) {
233                         analog->meaning->mq = SR_MQ_TEMPERATURE;
234                         if (temp_unit == 'F')
235                                 analog->meaning->unit = SR_UNIT_FAHRENHEIT;
236                         else
237                                 analog->meaning->unit = SR_UNIT_CELSIUS;
238                 }
239
240                 /*
241                  * Remove the MIN/MAX/AVG indicators when all of them
242                  * are shown at the same time.
243                  */
244                 ind1 = buf[1];
245                 if ((ind1 & 0xe0) == 0xe0)
246                         ind1 &= ~0xe0;
247
248                 /* AC/DC/Auto flags. */
249                 if (buf[1] & 0x10)
250                         analog->meaning->mqflags |= SR_MQFLAG_DC;
251                 if (buf[2] & 0x01)
252                         analog->meaning->mqflags |= SR_MQFLAG_AC;
253                 if (buf[1] & 0x01)
254                         analog->meaning->mqflags |= SR_MQFLAG_AUTORANGE;
255                 if (buf[1] & 0x08)
256                         analog->meaning->mqflags |= SR_MQFLAG_HOLD;
257                 if (ind1 & 0x20)
258                         analog->meaning->mqflags |= SR_MQFLAG_MAX;
259                 if (ind1 & 0x40)
260                         analog->meaning->mqflags |= SR_MQFLAG_MIN;
261                 if (ind1 & 0x80)
262                         analog->meaning->mqflags |= SR_MQFLAG_AVG;
263                 if (buf[3] & 0x01)
264                         analog->meaning->mqflags |= SR_MQFLAG_RELATIVE;
265
266                 /*
267                  * Remove the "dBm" indication's "m" indicator before the
268                  * SI unit's prefixes get inspected. To avoid an interaction
269                  * with the "milli" prefix.
270                  */
271                 ind15 = buf[15];
272                 if (ind15 & 0x02)
273                         ind15 &= ~0x04;
274
275                 /* SI prefix. */
276                 scale = 0;
277                 if (buf[14] & 0x40) /* n */
278                         scale = -9;
279                 if (buf[15] & 0x08) /* u */
280                         scale = -6;
281                 if (ind15 & 0x04) /* m */
282                         scale = -3;
283                 if (buf[15] & 0x40) /* k */
284                         scale = +3;
285                 if (buf[15] & 0x20) /* M */
286                         scale = +6;
287                 if (scale) {
288                         *floatval *= pow(10, scale);
289                         digits += -scale;
290                 }
291
292                 if (over_limit)
293                         *floatval = INFINITY;
294
295                 analog->encoding->digits  = digits;
296                 analog->spec->spec_digits = digits;
297         } else if (ch_idx == 1) {
298                 /*
299                  * Secondary display. Also inspect _some_ primary display
300                  * data, to determine the secondary display's validity.
301                  */
302                 (void)brymen_bm86x_parse_digits(&buf[2], 6, txtbuf,
303                         NULL, &temp_unit, NULL, 0x80);
304                 ret = brymen_bm86x_parse_digits(&buf[9], 4, txtbuf,
305                         floatval, NULL, &digits, 0x10);
306                 if (ret != SR_OK)
307                         return;
308
309                 /* SI unit. */
310                 if (buf[14] & 0x08) {
311                         analog->meaning->mq = SR_MQ_VOLTAGE;
312                         analog->meaning->unit = SR_UNIT_VOLT;
313                 } else if (buf[9] & 0x04) {
314                         analog->meaning->mq = SR_MQ_CURRENT;
315                         analog->meaning->unit = SR_UNIT_AMPERE;
316                 } else if (buf[9] & 0x08) {
317                         analog->meaning->mq = SR_MQ_CURRENT;
318                         analog->meaning->unit = SR_UNIT_PERCENTAGE;
319                 } else if (buf[14] & 0x04) {
320                         analog->meaning->mq = SR_MQ_FREQUENCY;
321                         analog->meaning->unit = SR_UNIT_HERTZ;
322                 } else if ((buf[9] & 0x40) && temp_unit) {
323                         analog->meaning->mq = SR_MQ_TEMPERATURE;
324                         if (temp_unit == 'F')
325                                 analog->meaning->unit = SR_UNIT_FAHRENHEIT;
326                         else
327                                 analog->meaning->unit = SR_UNIT_CELSIUS;
328                 }
329
330                 /* AC flag. */
331                 if (buf[9] & 0x20)
332                         analog->meaning->mqflags |= SR_MQFLAG_AC;
333
334                 /* SI prefix. */
335                 scale = 0;
336                 if (buf[ 9] & 0x01) /* u */
337                         scale = -6;
338                 if (buf[ 9] & 0x02) /* m */
339                         scale = -3;
340                 if (buf[14] & 0x02) /* k */
341                         scale = +3;
342                 if (buf[14] & 0x01) /* M */
343                         scale = +6;
344                 if (scale) {
345                         *floatval *= pow(10, scale);
346                         digits += -scale;
347                 }
348
349                 analog->encoding->digits  = digits;
350                 analog->spec->spec_digits = digits;
351         }
352
353         if (buf[9] & 0x80)
354                 sr_warn("Battery is low.");
355 }
356
357 SR_PRIV int sr_brymen_bm86x_parse(const uint8_t *buf, float *val,
358         struct sr_datafeed_analog *analog, void *info)
359 {
360         struct brymen_bm86x_info *info_local;
361         size_t ch_idx;
362
363         /*
364          * Scan a portion of the received DMM packet which corresponds
365          * to the caller's specified display. Then prepare to scan a
366          * different portion of the packet for another display. This
367          * routine gets called multiple times for one received packet.
368          */
369         info_local = info;
370         ch_idx = info_local->ch_idx;
371         brymen_bm86x_parse(buf, val, analog, ch_idx);
372         info_local->ch_idx = ch_idx + 1;
373
374         return SR_OK;
375 }