]> sigrok.org Git - libsigrok.git/blob - src/dmm/bm52x.c
dmm/bm52x: introduce support for Brymen BM525s
[libsigrok.git] / src / dmm / bm52x.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-2020 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 BM52x serial protocol parser. The USB protocol (for the cable)
25  * and the packet description (for the meter) were retrieved from:
26  * http://brymen.com/product-html/Download2.html
27  * http://brymen.com/product-html/PD02BM520s_protocolDL.html
28  * http://brymen.com/product-html/images/DownloadList/ProtocolList/BM520-BM520s_List/BM520-BM520s-10000-count-professional-dual-display-mobile-logging-DMMs-protocol.zip
29  */
30
31 /*
32  * TODO
33  * - This DMM packet parser exclusively supports live readings (vendor
34  *   documentation refers to it as "real-time download" aka RTD). A HID
35  *   report is sent which results in three HID reports in response which
36  *   carry 24 bytes with LCD indicator bitfields (and a few literals to
37  *   synchronize to the byte stream). Reading back previous recordings
38  *   ("memory data sets" in the vendor documentation) involve different
39  *   types of requests, and several of them, and result in a different
40  *   number of response reports while their interpretation differs, too,
41  *   of course. None of this fits the serial-dmm approach, and needs to
42  *   get addressed later.
43  *   - Configurable sample rate, range 20/s rate up to 600s period.
44  *   - Multiple sessions, one function per session, up to 999 "session
45  *     pages" (recordings with their sequence of measurement values).
46  *   - Up to 87000 (single display) or 43500 (dual display) measurements
47  *     total on BM525s.
48  *   - Request 0x00, 0x00, 0x52, 0x88 to request the HEAD of recordings.
49  *     Request 0x00, 0x00, 0x52, 0x89 to request the NEXT memory chunk.
50  *     Request 0x00, 0x00, 0x52, 0x8a to re-request the CURR memory chunk
51  *     (repetition when transmission failed detectably?).
52  *   - All these HID report requests result in four HID responses which
53  *     carry 32 bytes (24 bytes of payload data, and a checksum) where
54  *     application's fields can cross the boundary of HID reports and
55  *     even response chunks.
56  * - Some of the meter's functions and indications cannot get expressed
57  *   by means of sigrok MQ and flags terms. Some indicator's meaning is
58  *   unknown or uncertain, and thus their state is not evaluated.
59  *   - MAX-MIN, the span between extreme values, referred to as Vp-p.
60  *   - AVG is not available in BM525s and BM521s.
61  *   - LoZ, eliminating ghost voltages.
62  *   - LPF, low pass filter.
63  *   - dBm is a BM829s feature only, not available in BM525s.
64  *   - low battery, emits sr_warn() but isn't seen in the feed.
65  *   - @, 4-20mA loop, % (main display, left hand side), Hi/Lo. Some of
66  *     these are in the vendor's documentation for the DMM packet but not
67  *     supported by the BM525s device which motivated the creation of the
68  *     parser's and was used to test its operation.
69  *   - It's a guess that the many undocumented bits (44 of them) are
70  *     related to the bargraph (40 ticks, overflow, sign, 6/10 scale).
71  *   - Should T1-T2 have a delta ("relative") decoration? But the meter's
72  *     "relative" feature is flexible, accepts any display value as the
73  *     reference, including min/max/diff when displayed upon activation.
74  *   - The "beep jack" displays "InEr" in the secondary display. This is
75  *     not caught here, no PC side message gets emitted.
76  */
77
78 #include <config.h>
79 #include <libsigrok/libsigrok.h>
80 #include "libsigrok-internal.h"
81 #include <math.h>
82 #include <string.h>
83
84 #define LOG_PREFIX "brymen-bm52x"
85
86 #ifdef HAVE_SERIAL_COMM
87 SR_PRIV int sr_brymen_bm52x_packet_request(struct sr_serial_dev_inst *serial)
88 {
89         static const uint8_t request[] = { 0x00, 0x00, 0x52, 0x66, };
90
91         serial_write_nonblocking(serial, request, sizeof(request));
92
93         return SR_OK;
94 }
95 #endif
96
97 SR_PRIV gboolean sr_brymen_bm52x_packet_valid(const uint8_t *buf)
98 {
99         if (buf[16] != 0x52)
100                 return FALSE;
101         if (buf[17] != 0x52)
102                 return FALSE;
103         if (buf[18] != 0x52)
104                 return FALSE;
105         if (buf[19] != 0x52)
106                 return FALSE;
107
108         return TRUE;
109 }
110
111 /*
112  * Data bytes in the DMM packet encode LCD segments in an unusual order
113  * (bgcpafed) and in an unusual position (bit 4 being the decimal point
114  * for some digits, an additional indicator for others). Fortunately all
115  * eight digits encode their segments in identical ways across the bytes.
116  *
117  * These routines convert LCD segments to characters, and a section of the
118  * DMM packet (which corresponds to the primary or secondary display) to
119  * the text representation of the measurement's value, before regular text
120  * to number conversion is applied, and SI units and their prefixes get
121  * derived from more indicators. It's important to keep in mind similar
122  * indicators exist for main and secondary displays in different locations.
123  */
124
125 static char brymen_bm52x_parse_digit(uint8_t b)
126 {
127         switch (b & ~0x10) {
128         /* Sign. */
129         case 0x40: /* ------g */ return '-';
130         /* Decimal digits. */
131         case 0xaf: /* abcdef- */ return '0';
132         case 0xa0: /* -bc---- */ return '1';
133         case 0xcb: /* ab-de-g */ return '2';
134         case 0xe9: /* abcd--g */ return '3';
135         case 0xe4: /* -bc--fg */ return '4';
136         case 0x6d: /* a-cd-fg */ return '5';
137         case 0x6f: /* a-cdefg */ return '6';
138         case 0xa8: /* abc---- */ return '7';
139         case 0xef: /* abcdefg */ return '8';
140         case 0xed: /* abcd-fg */ return '9';
141         /* Temperature units. */
142         case 0x0f: /* a--def- */ return 'C';
143         case 0x4e: /* a---efg */ return 'F';
144         /* OL condition, and diode and "Auto" modes. */
145         case 0x07: /* ---def- */ return 'L';
146         case 0xe3: /* -bcde-g */ return 'd';
147         case 0x20: /* --c---- */ return 'i';
148         case 0x63: /* --cde-g */ return 'o';
149         case 0xee: /* abc-efg */ return 'A';
150         case 0x23: /* --cde-- */ return 'u';
151         case 0x47: /* ---defg */ return 't';
152         /* Blank digit. */
153         case 0x00: /* ------- */ return '\0';
154         /* Invalid or unknown segment combination. */
155         default:
156                 sr_warn("Unknown encoding for digit: 0x%02x.", b);
157                 return '\0';
158         }
159 }
160
161 static int brymen_bm52x_parse_digits(const uint8_t *pkt, size_t pktlen,
162         char *txtbuf, float *value, char *temp_unit, int *digits, int signflag)
163 {
164         uint8_t byte;
165         char *txtptr, txtchar;
166         size_t pos;
167         int ret;
168
169         txtptr = txtbuf;
170         if (digits)
171                 *digits = INT_MIN;
172
173         if (pkt[0] & signflag)
174                 *txtptr++ = '-';
175         for (pos = 0; pos < pktlen; pos++) {
176                 byte = pkt[1 + pos];
177                 txtchar = brymen_bm52x_parse_digit(byte);
178                 if (pos == 3 && (txtchar == 'C' || txtchar == 'F')) {
179                         if (temp_unit)
180                                 *temp_unit = txtchar;
181                 } else if (txtchar) {
182                         *txtptr++ = txtchar;
183                         if (digits)
184                                 (*digits)++;
185                 }
186                 if (pos < 3 && (byte & 0x10)) {
187                         *txtptr++ = '.';
188                         if (digits)
189                                 *digits = 0;
190                 }
191         }
192         *txtptr = '\0';
193
194         if (digits && *digits < 0)
195                 *digits = 0;
196
197         ret = value ? sr_atof_ascii(txtbuf, value) : SR_OK;
198         if (ret != SR_OK) {
199                 sr_dbg("invalid float string: '%s'", txtbuf);
200                 return ret;
201         }
202
203         return SR_OK;
204 }
205
206 /*
207  * Extract the measurement value and its properties for one of the
208  * meter's displays from the DMM packet.
209  */
210 static void brymen_bm52x_parse(const uint8_t *buf, float *floatval,
211         struct sr_datafeed_analog *analog, size_t ch_idx)
212 {
213         char txtbuf[16], temp_unit;
214         int ret, digits, scale;
215         int is_diode, is_auto, is_no_temp, is_ol, is_db, is_main_milli;
216         int is_mm_max, is_mm_min, is_mm_avg, is_mm_dash;
217
218         temp_unit = '\0';
219         if (ch_idx == 0) {
220                 /*
221                  * Main display. Note that _some_ of the second display's
222                  * indicators are involved in the inspection of the _first_
223                  * display's measurement value. So we have to get the
224                  * second display's text buffer here, too.
225                  */
226                 (void)brymen_bm52x_parse_digits(&buf[7], 4, txtbuf,
227                         NULL, NULL, NULL, 0);
228                 is_diode = strcmp(txtbuf, "diod") == 0;
229                 is_auto = strcmp(txtbuf, "Auto") == 0;
230                 ret = brymen_bm52x_parse_digits(&buf[2], 4, txtbuf,
231                         floatval, &temp_unit, &digits, 0x80);
232                 is_ol = strstr(txtbuf, "0L") || strstr(txtbuf, "0.L");
233                 is_no_temp = strcmp(txtbuf, "---C") == 0;
234                 is_no_temp |= strcmp(txtbuf, "---F") == 0;
235                 if (ret != SR_OK && !is_ol)
236                         return;
237
238                 /* SI unit, derived from meter's current function. */
239                 is_db = buf[6] & 0x10;
240                 is_main_milli = buf[14] & 0x40;
241                 if (buf[14] & 0x20) {
242                         analog->meaning->mq = SR_MQ_VOLTAGE;
243                         analog->meaning->unit = SR_UNIT_VOLT;
244                         if (is_diode) {
245                                 analog->meaning->mqflags |= SR_MQFLAG_DIODE;
246                                 analog->meaning->mqflags |= SR_MQFLAG_DC;
247                         }
248                 } else if (buf[14] & 0x10) {
249                         analog->meaning->mq = SR_MQ_CURRENT;
250                         analog->meaning->unit = SR_UNIT_AMPERE;
251                 } else if (buf[14] & 0x01) {
252                         analog->meaning->mq = SR_MQ_CAPACITANCE;
253                         analog->meaning->unit = SR_UNIT_FARAD;
254                 } else if (buf[14] & 0x02) {
255                         analog->meaning->mq = SR_MQ_CONDUCTANCE;
256                         analog->meaning->unit = SR_UNIT_SIEMENS;
257                 } else if (buf[13] & 0x10) {
258                         analog->meaning->mq = SR_MQ_FREQUENCY;
259                         analog->meaning->unit = SR_UNIT_HERTZ;
260                 } else if (buf[7] & 0x01) {
261                         analog->meaning->mq = SR_MQ_CONTINUITY;
262                         analog->meaning->unit = SR_UNIT_OHM;
263                 } else if (buf[13] & 0x20) {
264                         analog->meaning->mq = SR_MQ_RESISTANCE;
265                         analog->meaning->unit = SR_UNIT_OHM;
266                 } else if (is_db && is_main_milli) {
267                         analog->meaning->mq = SR_MQ_POWER;
268                         analog->meaning->unit = SR_UNIT_DECIBEL_MW;
269                 } else if (buf[14] & 0x04) {
270                         analog->meaning->mq = SR_MQ_DUTY_CYCLE;
271                         analog->meaning->unit = SR_UNIT_PERCENTAGE;
272                 } else if ((buf[2] & 0x09) && temp_unit) {
273                         if (is_no_temp)
274                                 return;
275                         analog->meaning->mq = SR_MQ_TEMPERATURE;
276                         if (temp_unit == 'F')
277                                 analog->meaning->unit = SR_UNIT_FAHRENHEIT;
278                         else
279                                 analog->meaning->unit = SR_UNIT_CELSIUS;
280                 }
281
282                 /*
283                  * Remove the MIN/MAX/AVG indicators when all of them
284                  * are shown at the same time (indicating that recording
285                  * is active, but live readings are shown). This also
286                  * removes the MAX-MIN (V p-p) indication which cannot
287                  * get represented by SR_MQFLAG_* means.
288                  *
289                  * Keep the check conditions separate to simplify future
290                  * maintenance when Vp-p gets added. Provide the value of
291                  * currently unsupported modes just without flags (show
292                  * the maximum amount of LCD content on screen that we
293                  * can represent in sigrok).
294                  */
295                 is_mm_max = buf[1] & 0x01;
296                 is_mm_min = buf[1] & 0x08;
297                 is_mm_avg = buf[1] & 0x02;
298                 is_mm_dash = buf[1] & 0x04;
299                 if (is_mm_max && is_mm_min && is_mm_avg)
300                         is_mm_max = is_mm_min = is_mm_avg = 0;
301                 if (is_mm_max && is_mm_min && is_mm_dash)
302                         is_mm_max = is_mm_min = 0;
303                 if (is_mm_max && is_mm_min && !is_mm_dash)
304                         is_mm_max = is_mm_min = 0;
305
306                 /* AC/DC/Auto flags. Hold/Min/Max/Rel etc flags. */
307                 if (buf[1] & 0x20)
308                         analog->meaning->mqflags |= SR_MQFLAG_DC;
309                 if (buf[1] & 0x10)
310                         analog->meaning->mqflags |= SR_MQFLAG_AC;
311                 if (buf[20] & 0x10)
312                         analog->meaning->mqflags |= SR_MQFLAG_AUTORANGE;
313                 if (buf[20] & 0x80)
314                         analog->meaning->mqflags |= SR_MQFLAG_HOLD;
315                 if (is_mm_max)
316                         analog->meaning->mqflags |= SR_MQFLAG_MAX;
317                 if (is_mm_min)
318                         analog->meaning->mqflags |= SR_MQFLAG_MIN;
319                 if (is_mm_avg)
320                         analog->meaning->mqflags |= SR_MQFLAG_AVG;
321                 if (buf[2] & 0x40)
322                         analog->meaning->mqflags |= SR_MQFLAG_RELATIVE;
323
324                 /*
325                  * Remove the "dBm" indication's "m" indicator before the
326                  * SI unit's prefixes get inspected. Avoids an interaction
327                  * with the "milli" prefix. Strictly speaking BM525s does
328                  * not support dBm, but other models do and we may want
329                  * to share the protocol parser.
330                  */
331                 if (is_db)
332                         is_main_milli = 0;
333
334                 /* SI prefix. */
335                 scale = 0;
336                 if (buf[14] & 0x08) /* n */
337                         scale = -9;
338                 if (buf[14] & 0x80) /* u */
339                         scale = -6;
340                 if (is_main_milli) /* m */
341                         scale = -3;
342                 if (buf[13] & 0x80) /* k */
343                         scale = +3;
344                 if (buf[13] & 0x40) /* M */
345                         scale = +6;
346                 if (scale) {
347                         *floatval *= pow(10, scale);
348                         digits += -scale;
349                 }
350
351                 if (is_ol)
352                         *floatval = INFINITY;
353
354                 analog->encoding->digits  = digits;
355                 analog->spec->spec_digits = digits;
356         } else if (ch_idx == 1) {
357                 /*
358                  * Secondary display. Also inspect _some_ primary display
359                  * data, to determine the secondary display's validity.
360                  */
361                 (void)brymen_bm52x_parse_digits(&buf[2], 4, txtbuf,
362                         NULL, &temp_unit, NULL, 0x80);
363                 ret = brymen_bm52x_parse_digits(&buf[7], 4, txtbuf,
364                         floatval, NULL, &digits, 0x20);
365                 is_diode = strcmp(txtbuf, "diod") == 0;
366                 is_auto = strcmp(txtbuf, "Auto") == 0;
367                 is_no_temp = strcmp(txtbuf, "---C") == 0;
368                 is_no_temp |= strcmp(txtbuf, "---F") == 0;
369                 if (is_diode || is_auto)
370                         return;
371                 if (is_no_temp)
372                         return;
373
374                 /* SI unit. */
375                 if (buf[12] & 0x10) {
376                         analog->meaning->mq = SR_MQ_VOLTAGE;
377                         analog->meaning->unit = SR_UNIT_VOLT;
378                 } else if (buf[12] & 0x20) {
379                         analog->meaning->mq = SR_MQ_CURRENT;
380                         if (buf[11] & 0x10)
381                                 analog->meaning->unit = SR_UNIT_PERCENTAGE;
382                         else
383                                 analog->meaning->unit = SR_UNIT_AMPERE;
384                 } else if (buf[13] & 0x02) {
385                         analog->meaning->mq = SR_MQ_RESISTANCE;
386                         analog->meaning->unit = SR_UNIT_OHM;
387                 } else if (buf[12] & 0x02) {
388                         analog->meaning->mq = SR_MQ_CONDUCTANCE;
389                         analog->meaning->unit = SR_UNIT_SIEMENS;
390                 } else if (buf[12] & 0x01) {
391                         analog->meaning->mq = SR_MQ_CAPACITANCE;
392                         analog->meaning->unit = SR_UNIT_FARAD;
393                 } else if (buf[7] & 0x06) {
394                         if (strstr(txtbuf, "---"))
395                                 return;
396                         analog->meaning->mq = SR_MQ_TEMPERATURE;
397                         if (temp_unit == 'F')
398                                 analog->meaning->unit = SR_UNIT_FAHRENHEIT;
399                         else
400                                 analog->meaning->unit = SR_UNIT_CELSIUS;
401                 } else if (buf[13] & 0x01) {
402                         analog->meaning->mq = SR_MQ_FREQUENCY;
403                         analog->meaning->unit = SR_UNIT_HERTZ;
404                 } else if (buf[11] & 0x08) {
405                         analog->meaning->mq = SR_MQ_DUTY_CYCLE;
406                         analog->meaning->unit = SR_UNIT_PERCENTAGE;
407                 }
408
409                 /* DC/AC flags. */
410                 if (buf[7] & 0x80)
411                         analog->meaning->mqflags |= SR_MQFLAG_DC;
412                 if (buf[7] & 0x40)
413                         analog->meaning->mqflags |= SR_MQFLAG_AC;
414
415                 /* SI prefix. */
416                 scale = 0;
417                 if (buf[12] & 0x04) /* n */
418                         scale = -9;
419                 if (buf[12] & 0x40) /* u */
420                         scale = -6;
421                 if (buf[12] & 0x80) /* m */
422                         scale = -3;
423                 if (buf[13] & 0x04) /* k */
424                         scale = +3;
425                 if (buf[13] & 0x08) /* M */
426                         scale = +6;
427                 if (scale) {
428                         *floatval *= pow(10, scale);
429                         digits += -scale;
430                 }
431
432                 analog->encoding->digits  = digits;
433                 analog->spec->spec_digits = digits;
434         }
435
436         if (buf[7] & 0x08)
437                 sr_warn("Battery is low.");
438 }
439
440 SR_PRIV int sr_brymen_bm52x_parse(const uint8_t *buf, float *val,
441         struct sr_datafeed_analog *analog, void *info)
442 {
443         struct brymen_bm52x_info *info_local;
444         size_t ch_idx;
445
446         /*
447          * Scan a portion of the received DMM packet which corresponds
448          * to the caller's specified display. Then prepare to scan a
449          * different portion of the packet for another display. This
450          * routine gets called multiple times for one received packet.
451          */
452         info_local = info;
453         ch_idx = info_local->ch_idx;
454         brymen_bm52x_parse(buf, val, analog, ch_idx);
455         info_local->ch_idx = ch_idx + 1;
456
457         return SR_OK;
458 }