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