]> sigrok.org Git - libsigrok.git/blame - src/dmm/bm86x.c
output/csv: use intermediate time_t var, silence compiler warning
[libsigrok.git] / src / dmm / bm86x.c
CommitLineData
0759e2f5
GS
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:
b189c901
FR
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
70a23b50 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
0759e2f5
GS
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
41SR_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
0759e2f5
GS
45 serial_write_nonblocking(serial, request, sizeof(request));
46
47 return SR_OK;
48}
49#endif
50
51SR_PRIV gboolean sr_brymen_bm86x_packet_valid(const uint8_t *buf)
52{
0759e2f5
GS
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
f1259129
GS
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
0759e2f5
GS
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
94static 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
127static 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 */
176static 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
e378e3a2 183 temp_unit = '\0';
0759e2f5
GS
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;
e378e3a2 232 } else if ((buf[2] & 0x0a) && temp_unit) {
0759e2f5
GS
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) {
e378e3a2
GS
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);
0759e2f5 304 ret = brymen_bm86x_parse_digits(&buf[9], 4, txtbuf,
e378e3a2 305 floatval, NULL, &digits, 0x10);
fbbbca84
GS
306 if (ret != SR_OK)
307 return;
0759e2f5
GS
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;
e378e3a2 322 } else if ((buf[9] & 0x40) && temp_unit) {
0759e2f5
GS
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
357SR_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}