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