]> sigrok.org Git - libsigrok.git/blame - src/dmm/bm85x.c
dmm/bm85x: introduce DMM packet parser for Brymen BM850(a/s)
[libsigrok.git] / src / dmm / bm85x.c
CommitLineData
27186eda
GS
1/*
2 * This file is part of the libsigrok project.
3 *
4 * Copyright (C) 2012 Alexandru Gagniuc <mr.nuke.me@gmail.com>
5 * Copyright (C) 2014 Aurelien Jacobs <aurel@gnuage.org>
6 * Copyright (C) 2019-2020 Gerhard Sittig <gerhard.sittig@gmx.net>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22/**
23 * @file
24 *
25 * Protocol parser for Brymen BM850s DMM packets. The USB protocol (for the
26 * cable) and the packet description (for the meter) were retrieved from:
27 * http://brymen.com/product-html/Download2.html
28 * http://brymen.com/product-html/PD02BM850s_protocolDL.html
29 * http://brymen.com/product-html/images/DownloadList/ProtocolList/BM850-BM850a-BM850s_List/BM850-BM850a-BM850s-500000-count-DMM-protocol-BC85X-BC85Xa.zip
30 *
31 * Implementor's notes on the protocol:
32 * - The BM85x devices require a low RTS pulse after COM port open and
33 * before communication of requests and responses. The vendor doc
34 * recommends 100ms pulse width including delays around it. Without
35 * that RTS pulse the meter won't respond to requests.
36 * - The request has a three byte header (DLE, STX, command code), two
37 * bytes command arguments, and three bytes tail (checksum, DLE, ETX).
38 * The checksum spans the area (including) the command code and args.
39 * The checksum value is the XOR across all payload bytes. Exclusively
40 * command 0x00 is used (initiate next measurement response) which does
41 * not need arguments (passes all-zero values).
42 * - The response has a four byte header (DLE, STX, command code, payload
43 * size), the respective number of payload data bytes, and a three byte
44 * tail (checksum, DLE, ETX). The checksum spans the range after the
45 * length field and before the checksum field. Command 0 response data
46 * payload consists of a four byte flags field and a text field for
47 * measurement values (floating point with exponent in ASCII).
48 * - Special cases of response data:
49 * - The text field which carries the measurement value also contains
50 * whitespace which may break simple text to number conversion. Like
51 * 10 02 00 0f 07 00 00 00 20 30 2e 30 30 33 32 20 45 2b 30 46 10 03
52 * which translates to: 07 00 00 00 " 0.0032 E+0". Text for overload
53 * conditions can be shorter which results in variable packet length.
54 * Some meter functions provide unexpected text for their values.
55 * - The reference impedance for decibel measurements looks wrong and
56 * requires special treatment to isolate the 4..1200R value:
57 * bfunc 80 20 00 00, text " 0. 800 E+1" (reference, 800R)
58 * The decibel measurement values use an unexpected scale.
59 * bfunc 00 20 00 00, text "-0.3702 E-1" (measurement, -37.02dBm)
60 * The reference value gets sent (sometimes) in a DMM response when
61 * the meter's function is entered, or the reference value changes.
62 * The 'bfunc' flags combination allows telling packet types apart.
63 * - Temperature measurements put the C/F unit between the mantissa
64 * and the exponent, which needs to get removed: " 0.0217CE+3"
65 * - Diode measurements appear to exclusively provide the 'Volt' flag
66 * but no 'Diode' flag. The display shows ".diod" for a moment but
67 * this information is no longer available when voltage measurements
68 * are seen.
69 */
70
71#include <config.h>
72#include <ctype.h>
73#include <math.h>
74#include <libsigrok/libsigrok.h>
75#include "libsigrok-internal.h"
76#include <string.h>
77
78#define LOG_PREFIX "brymen-bm85x"
79
80#define STX 0x02
81#define ETX 0x03
82#define DLE 0x10
83
84#define CMD_GET_READING 0
85
86#define PKT_HEAD_LEN 4
87#define PKT_DATA_MAX 15
88#define PKT_TAIL_LEN 3
89#define PKT_BFUNC_LEN 4
90
91static uint8_t bm85x_crc(const uint8_t *buf, size_t len)
92{
93 uint8_t crc;
94
95 crc = 0;
96 while (len--)
97 crc ^= *buf++;
98
99 return crc;
100}
101
102#ifdef HAVE_SERIAL_COMM
103/** Meter's specific activity after port open and before data exchange. */
104SR_PRIV int brymen_bm85x_after_open(struct sr_serial_dev_inst *serial)
105{
106 int rts_toggle_delay_us;
107
108 /*
109 * The device requires an RTS *pulse* before communication.
110 * The vendor's documentation recommends the following sequence:
111 * Open the COM port, wait for 100ms, set RTS=1, wait for 100ms,
112 * set RTS=0, wait for 100ms, set RTS=1, configure bitrate and
113 * frame format, transmit request data, receive response data.
114 */
115 rts_toggle_delay_us = 100 * 1000; /* 100ms */
116 g_usleep(rts_toggle_delay_us);
117 serial_set_handshake(serial, 1, -1);
118 g_usleep(rts_toggle_delay_us);
119 serial_set_handshake(serial, 0, -1);
120 g_usleep(rts_toggle_delay_us);
121 serial_set_handshake(serial, 1, -1);
122 g_usleep(rts_toggle_delay_us);
123
124 return SR_OK;
125}
126
127static int bm85x_send_command(struct sr_serial_dev_inst *serial,
128 uint8_t cmd, uint8_t arg1, uint8_t arg2)
129{
130 uint8_t buf[8];
131 uint8_t crc, *wrptr, *crcptr;
132 size_t wrlen;
133 int ret;
134
135 wrptr = &buf[0];
136 write_u8_inc(&wrptr, DLE);
137 write_u8_inc(&wrptr, STX);
138 crcptr = wrptr;
139 write_u8_inc(&wrptr, cmd);
140 write_u8_inc(&wrptr, arg1);
141 write_u8_inc(&wrptr, arg2);
142 crc = bm85x_crc(crcptr, wrptr - crcptr);
143 write_u8_inc(&wrptr, crc);
144 write_u8_inc(&wrptr, DLE);
145 write_u8_inc(&wrptr, ETX);
146
147 wrlen = wrptr - &buf[0];
148 ret = serial_write_nonblocking(serial, &buf[0], wrlen);
149 if (ret < 0)
150 return ret;
151 if ((size_t)ret != wrlen)
152 return SR_ERR_IO;
153
154 return SR_OK;
155}
156
157/** Initiate reception of another meter's reading. */
158SR_PRIV int brymen_bm85x_packet_request(struct sr_serial_dev_inst *serial)
159{
160 return bm85x_send_command(serial, CMD_GET_READING, 0, 0);
161}
162#endif
163
164/**
165 * Check Brymen BM85x DMM packet for validity.
166 *
167 * @param[in] st The DMM driver's internal state.
168 * @param[in] buf The data bytes received so far.
169 * @param[in] len The received data's length (byte count).
170 * @param[out] pkt_len The packet's calculated total size (when valid).
171 *
172 * The BM850s protocol uses packets of variable length. A minimum amount
173 * of RX data provides the packet header, which communicates the payload
174 * size, which allows to determine the packet's total size. Callers of
175 * this validity checker can learn how much data will get consumed when
176 * a valid packet got received and processed. The packet size is not
177 * known in advance.
178 *
179 * @returns SR_OK when the packet is valid.
180 * @returns SR_ERR* (below zero) when the packet is invalid.
181 * @returns Greater 0 when packet is incomplete, more data is needed.
182 */
183SR_PRIV int brymen_bm85x_packet_valid(void *st,
184 const uint8_t *buf, size_t len, size_t *pkt_len)
185{
186 size_t plen;
187 uint8_t cmd, crc;
188
189 (void)st;
190
191 /* Four header bytes: DLE, STX, command, payload length. */
192 if (len < PKT_HEAD_LEN)
193 return SR_PACKET_NEED_RX;
194 if (read_u8_inc(&buf) != DLE)
195 return SR_PACKET_INVALID;
196 if (read_u8_inc(&buf) != STX)
197 return SR_PACKET_INVALID;
198 cmd = read_u8_inc(&buf);
199 /* Non-fatal, happens with OL pending during connect. */
200 if (cmd == 0x01)
201 cmd = 0x00;
202 if (cmd != CMD_GET_READING)
203 return SR_PACKET_INVALID;
204 plen = read_u8_inc(&buf);
205 if (plen > PKT_DATA_MAX)
206 return SR_PACKET_INVALID;
207 len -= PKT_HEAD_LEN;
208
209 /* Checksum spans bfunc and value text. Length according to header. */
210 if (len < plen + PKT_TAIL_LEN)
211 return SR_PACKET_NEED_RX;
212 crc = bm85x_crc(buf, plen);
213 buf += plen;
214 len -= plen;
215
216 /* Three tail bytes: checksum, DLE, ETX. */
217 if (len < PKT_TAIL_LEN)
218 return SR_PACKET_NEED_RX;
219 if (read_u8_inc(&buf) != crc)
220 return SR_PACKET_INVALID;
221 if (read_u8_inc(&buf) != DLE)
222 return SR_PACKET_INVALID;
223 if (read_u8_inc(&buf) != ETX)
224 return SR_PACKET_INVALID;
225
226 /*
227 * Only return the total packet length when the receive buffer
228 * was found to be valid. For invalid packets it's preferred to
229 * have the caller keep trying to sync to the packet stream.
230 */
231 if (pkt_len)
232 *pkt_len = PKT_HEAD_LEN + plen + PKT_TAIL_LEN;
233 return SR_PACKET_VALID;
234}
235
236struct bm85x_flags {
237 gboolean is_batt, is_db, is_perc, is_hz, is_amp, is_beep;
238 gboolean is_ohm, is_temp_f, is_temp_c, is_diode, is_cap;
239 gboolean is_volt, is_dc, is_ac;
240};
241
242static int bm85x_parse_flags(const uint8_t *bfunc, struct bm85x_flags *flags)
243{
244 if (!bfunc || !flags)
245 return SR_ERR_ARG;
246 memset(flags, 0, sizeof(*flags));
247
248 flags->is_batt = bfunc[3] & (1u << 7);
249 if ((bfunc[3] & 0x7f) != 0)
250 return SR_ERR_ARG;
251
252 if ((bfunc[2] & 0xff) != 0)
253 return SR_ERR_ARG;
254
255 if ((bfunc[1] & 0xc0) != 0)
256 return SR_ERR_ARG;
257 flags->is_db = bfunc[1] & (1u << 5);
258 if ((bfunc[1] & 0x10) != 0)
259 return SR_ERR_ARG;
260 flags->is_perc = bfunc[1] & (1u << 3);
261 flags->is_hz = bfunc[1] & (1u << 2);
262 flags->is_amp = bfunc[1] & (1u << 1);
263 flags->is_beep = bfunc[1] & (1u << 0);
264
265 flags->is_ohm = bfunc[0] & (1u << 7);
266 flags->is_temp_f = bfunc[0] & (1u << 6);
267 flags->is_temp_c = bfunc[0] & (1u << 5);
268 flags->is_diode = bfunc[0] & (1u << 4);
269 flags->is_cap = bfunc[0] & (1u << 3);
270 flags->is_volt = bfunc[0] & (1u << 2);
271 flags->is_dc = bfunc[0] & (1u << 1);
272 flags->is_ac = bfunc[0] & (1u << 0);
273
274 return SR_OK;
275}
276
277static int bm85x_parse_value(char *txt, double *val, int *digits)
278{
279 char *src, *dst, c;
280 int ret;
281
282 /*
283 * See above comment on whitespace in response's number text.
284 * The caller provides a NUL terminated writable text copy.
285 * Go for low hanging fruit first (OL condition). Eliminate
286 * whitespace then and do the number conversion.
287 */
288 if (strstr(txt, "+OL")) {
289 *val = +INFINITY;
290 return SR_OK;
291 }
292 if (strstr(txt, "-OL")) {
293 *val = -INFINITY;
294 return SR_OK;
295 }
296 if (strstr(txt, "OL")) {
297 *val = INFINITY;
298 return SR_OK;
299 }
300
301 src = txt;
302 dst = txt;
303 while (*src) {
304 c = *src++;
305 if (c == ' ')
306 continue;
307 *dst++ = c;
308 }
309 *dst = '\0';
310
311 ret = sr_atod_ascii_digits(txt, val, digits);
312 if (ret != SR_OK)
313 return ret;
314
315 return SR_OK;
316}
317
318static int bm85x_parse_payload(const uint8_t *p, size_t l,
319 double *val, struct sr_datafeed_analog *analog)
320{
321 const uint8_t *bfunc;
322 char text_buf[PKT_DATA_MAX], *text;
323 size_t text_len;
324 int ret;
325 struct bm85x_flags flags;
326 int digits;
327 char *parse;
328
329 /* Get a bfunc bits reference, and a writable value text. */
330 bfunc = &p[0];
331 text_len = l - PKT_BFUNC_LEN;
332 memcpy(text_buf, &p[PKT_BFUNC_LEN], text_len);
333 text_buf[text_len] = '\0';
334 text = &text_buf[0];
335 sr_dbg("DMM bfunc %02x %02x %02x %02x, text \"%s\"",
336 bfunc[0], bfunc[1], bfunc[2], bfunc[3], text);
337
338 /* Check 'bfunc' bitfield first, text interpretation depends on it. */
339 ret = bm85x_parse_flags(bfunc, &flags);
340 if (ret != SR_OK)
341 return ret;
342
343 /* Parse the text after potential normalization/transformation. */
344 if (flags.is_db && flags.is_ohm) {
345 static const char *prefix = " 0.";
346 static const char *suffix = " E";
347 /* See above comment on dBm reference value text. */
348 if (strncmp(text, prefix, strlen(prefix)) != 0)
349 return SR_ERR_DATA;
350 text += strlen(prefix);
351 text_len -= strlen(prefix);
352 parse = strstr(text, suffix);
353 if (!parse)
354 return SR_ERR_DATA;
355 *parse = '\0';
356 }
357 if (flags.is_temp_f || flags.is_temp_c) {
358 /* See above comment on temperature value text. */
359 parse = strchr(text, flags.is_temp_f ? 'F' : 'C');
360 if (!parse)
361 return SR_ERR_DATA;
362 *parse = ' ';
363 }
364 digits = 0;
365 ret = bm85x_parse_value(text, val, &digits);
366 if (ret != SR_OK)
367 return ret;
368
369 /* Fill in MQ and flags result details. */
370 analog->meaning->mqflags = 0;
371 if (flags.is_volt) {
372 analog->meaning->mq = SR_MQ_VOLTAGE;
373 analog->meaning->unit = SR_UNIT_VOLT;
374 }
375 if (flags.is_amp) {
376 analog->meaning->mq = SR_MQ_CURRENT;
377 analog->meaning->unit = SR_UNIT_AMPERE;
378 }
379 if (flags.is_ohm) {
380 if (flags.is_db)
381 analog->meaning->mq = SR_MQ_RESISTANCE;
382 else if (flags.is_beep)
383 analog->meaning->mq = SR_MQ_CONTINUITY;
384 else
385 analog->meaning->mq = SR_MQ_RESISTANCE;
386 analog->meaning->unit = SR_UNIT_OHM;
387 }
388 if (flags.is_hz) {
389 analog->meaning->mq = SR_MQ_FREQUENCY;
390 analog->meaning->unit = SR_UNIT_HERTZ;
391 }
392 if (flags.is_perc) {
393 analog->meaning->mq = SR_MQ_DUTY_CYCLE;
394 analog->meaning->unit = SR_UNIT_PERCENTAGE;
395 }
396 if (flags.is_cap) {
397 analog->meaning->mq = SR_MQ_CAPACITANCE;
398 analog->meaning->unit = SR_UNIT_FARAD;
399 }
400 if (flags.is_temp_f) {
401 analog->meaning->mq = SR_MQ_TEMPERATURE;
402 analog->meaning->unit = SR_UNIT_FAHRENHEIT;
403 }
404 if (flags.is_temp_c) {
405 analog->meaning->mq = SR_MQ_TEMPERATURE;
406 analog->meaning->unit = SR_UNIT_CELSIUS;
407 }
408 if (flags.is_db && !flags.is_ohm) {
409 /* See above comment on dBm measurements scale. */
410 analog->meaning->mq = SR_MQ_POWER;
411 analog->meaning->unit = SR_UNIT_DECIBEL_MW;
412 *val *= 1000;
413 digits -= 3;
414 }
415
416 if (flags.is_diode) {
417 /* See above comment on diode measurement responses. */
418 analog->meaning->mq = SR_MQ_VOLTAGE;
419 analog->meaning->unit = SR_UNIT_VOLT;
420 analog->meaning->mqflags |= SR_MQFLAG_DIODE;
421 analog->meaning->mqflags |= SR_MQFLAG_DC;
422 }
423 if (flags.is_ac)
424 analog->meaning->mqflags |= SR_MQFLAG_AC;
425 if (flags.is_dc)
426 analog->meaning->mqflags |= SR_MQFLAG_DC;
427
428 analog->encoding->digits = digits;
429 analog->spec->spec_digits = digits;
430
431 if (flags.is_batt)
432 sr_warn("Low battery!");
433
434 return SR_OK;
435}
436
437SR_PRIV int brymen_bm85x_parse(void *st, const uint8_t *buf, size_t len,
438 double *val, struct sr_datafeed_analog *analog, void *info)
439{
440 const uint8_t *pl_ptr;
441 size_t pl_len;
442
443 (void)st;
444 (void)info;
445
446 if (!buf || !len)
447 return SR_ERR_DATA;
448 if (!val || !analog)
449 return SR_ERR_DATA;
450
451 if (brymen_bm85x_packet_valid(NULL, buf, len, NULL) != SR_PACKET_VALID)
452 return SR_ERR_DATA;
453 pl_ptr = &buf[PKT_HEAD_LEN];
454 pl_len = len - PKT_HEAD_LEN - PKT_TAIL_LEN;
455
456 return bm85x_parse_payload(pl_ptr, pl_len, val, analog);
457}