]>
Commit | Line | Data |
---|---|---|
400bc4ff 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-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 | } |