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