]> sigrok.org Git - libsigrok.git/blob - src/hardware/devantech-eth008/protocol.c
output/csv: use intermediate time_t var, silence compiler warning
[libsigrok.git] / src / hardware / devantech-eth008 / protocol.c
1 /*
2  * This file is part of the libsigrok project.
3  *
4  * Copyright (C) 2023 Gerhard Sittig <gerhard.sittig@gmx.net>
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /*
21  * Communicate to the Devantech ETH008 relay card via TCP and Ethernet.
22  * Also supports other cards when their protocol is similar enough.
23  * USB and Modbus attached cards are not covered by this driver.
24  *
25  * See http://www.robot-electronics.co.uk/files/eth008b.pdf for device
26  * capabilities and a protocol discussion. See other devices' documents
27  * for additional features (digital input, analog input, TCP requests
28  * which ETH008 does not implement).
29  * See https://github.com/devantech/devantech_eth_python for MIT licensed
30  * Python source code which is maintained by the vendor.
31  * This sigrok driver implementation was created based on information in
32  * version 0.1.2 of the Python code (corresponds to commit 0c0080b88e29),
33  * and PDF files that are referenced in the shop's product pages (which
34  * also happen to provide ZIP archives with examples that are written
35  * using other programming languages).
36  *
37  * The device provides several means of communication: HTTP requests
38  * (as well as an interactive web form). Raw TCP communication with
39  * binary requests and responses. Text requests and responses over
40  * TCP sockets. Some of these depend on the firmware version. Version
41  * checks before command transmission is essentially non-existent in
42  * this sigrok driver implementation. Binary transmission is preferred
43  * because it is assumed that this existed in all firmware versions.
44  * The firmware interestingly accepts concurrent network connections
45  * (up to five of them, all share the same password). Which means that
46  * the peripheral's state can change even while we are controlling it.
47  *
48  * TCP communication seems to rely on network fragmentation and assumes
49  * that software stacks provide all of a request in a single receive
50  * call on the firmware side. Which works for local communication, but
51  * could become an issue when long distances and tunnels are involved.
52  * This sigrok driver also assumes complete reception within a single
53  * receive call. The short length of binary transmission helps here
54  * (the largest payloads has a length of four bytes).
55  *
56  * The lack of length specs as well as termination in the protocol
57  * (both binary as well as text variants over TCP sockets) results in
58  * the inability to synchronize to the firmware when connecting and
59  * after hiccups in an established connection. The fixed length of
60  * requests and responses for binary payloads helps a little bit,
61  * assuming that TCP connect is used to recover. The overhead of
62  * HTTP requests and responses is considered undesirable for this
63  * sigrok driver implementation. [This also means that a transport
64  * which lacks the concept of network frames cannot send passwords.]
65  * The binary transport appears to lack HELLO or NOP requests that
66  * could be used to synchronize. Firmware just would not respond to
67  * unsupported commands. Maybe a repeated sequence of identity reads
68  * combined with a read timeout could help synchronize, but only if
69  * the response is known because the model was identified before.
70  *
71  * The sigrok driver source code was phrased with the addition of more
72  * models in mind. Only few code paths require adjustment when similar
73  * variants of requests or responses are involved in the communication
74  * to relay cards that support between two and twenty channels. Chances
75  * are good, existing firmware is compatible across firmware versions,
76  * and even across hardware revisions (model upgrades). Firmware just
77  * happens to not respond to unknown requests.
78  *
79  * Support for models with differing features also was kept somehwat
80  * simple and straight forward. The mapping of digital outputs to relay
81  * numbers from the user's perspective is incomplete for those cards
82  * where users decide whether relays are attached to digital outputs.
83  * When an individual physical channel can be operated in different
84  * modes, or when its value gets presented in different formats, then
85  * these values are not synchronized. This applies for digital inputs
86  * which are the result of applying a threshold to an analog value.
87  *
88  * TODO
89  * - Add support for other models.
90  *   - The Ethernet (and Wifi) devices should work as they are with
91  *     the current implementation.
92  *     https://www.robot-electronics.co.uk/files/eth484b.pdf.
93  *   - USB could get added here with reasonable effort. Serial over
94  *     CDC is transparently supported (lack of framing prevents the
95  *     use of variable length requests or responses, but should not
96  *     apply to these models anyway). The protocol radically differs
97  *     from Ethernet variants:
98  *     https://www.robot-electronics.co.uk/files/usb-rly16b.pdf
99  *     - 0x38 get serial number, yields 8 bytes
100  *     - 0x5a get software version, yields module ID 9, 1 byte version
101  *     - 0x5b get relay states, yields 1 byte current state
102  *     - 0x5c set relay state, takes 1 byte for all 8 relays
103  *     - 0x5d get supply voltage, yields 1 byte in 0.1V steps
104  *     - 0x5e set individual relay, takes 3 more bytes: relay number,
105  *       hi/lo pulse time in 10ms steps
106  *     - for interactive use? 'd' all relays on, 'e'..'l' relay 1..8 on,
107  *       'n' all relays off, 'o'..'v' relay 1..8 off
108  *   - Modbus may or may not be a good match for this driver, or may
109  *     better be kept in yet another driver? Requests and responses
110  *     again differ from Ethernet and USB models, refer to traditional
111  *     "coils" and have their individual and grouped access.
112  *     https://www.robot-electronics.co.uk/files/mbh88.pdf
113  * - Reconsider the relation of relay channels, and digital outputs
114  *   and their analog sampling and digital input interpretation. The
115  *   current implementation is naive, assumes the simple DO/DI/AI
116  *   groups and ignores their interaction within the firmware.
117  * - Add support for password protection?
118  *   - See command 0x79 to "login" (beware of the differing return value
119  *     compared to other commands), command 0x7a to check if passwords
120  *     are involved and whether the login needs refreshing, command 0x7b
121  *     for immediate "logout" in contrast to expiration.
122  *   - Alternatively consider switching to the "text protocol" in that
123  *     use case, which can send an optional password in every request
124  *     that controls relays (command 0x3a).
125  *   - How to specify the password in applications and how to pass them
126  *     to this driver is yet another issue that needs consideration.
127  */
128
129 #include "config.h"
130
131 #include <string.h>
132
133 #include "protocol.h"
134
135 #define READ_TIMEOUT_MS 20
136
137 enum cmd_code {
138         CMD_GET_MODULE_INFO = 0x10,
139         CMD_DIGITAL_ACTIVE = 0x20,
140         CMD_DIGITAL_INACTIVE = 0x21,
141         CMD_DIGITAL_SET_OUTPUTS = 0x23,
142         CMD_DIGITAL_GET_OUTPUTS = 0x24,
143         CMD_DIGITAL_GET_INPUTS = 0x25,
144         CMD_DIGITAL_ACTIVE_1MS = 0x26,
145         CMD_DIGITAL_INACTIVE_1MS = 0x27,
146         CMD_ANALOG_GET_INPUT = 0x32,
147         CMD_ANALOG_GET_INPUT_12BIT = 0x33,
148         CMD_ANALOG_GET_ALL_VOLTAGES = 0x34,
149         CMD_ASCII_TEXT_COMMAND = 0x3a,
150         CMD_GET_SERIAL_NUMBER = 0x77,
151         CMD_GET_SUPPLY_VOLTS = 0x78,
152         CMD_PASSWORD_ENTRY = 0x79,
153         CMD_GET_UNLOCK_TIME = 0x7a,
154         CMD_IMMEDIATE_LOGOUT = 0x7b,
155 };
156
157 /*
158  * Transmit a request to the relay card. Checks that all bytes get sent,
159  * short writes are considered fatal.
160  */
161 static int send_request(struct sr_serial_dev_inst *ser,
162         const uint8_t *data, size_t dlen)
163 {
164         int ret;
165         size_t written;
166
167         if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
168                 GString *txt = sr_hexdump_new(data, dlen);
169                 sr_spew("TX --> %s.", txt->str);
170                 sr_hexdump_free(txt);
171         }
172         ret = serial_write_blocking(ser, data, dlen, 0);
173         if (ret < 0)
174                 return ret;
175         written = (size_t)ret;
176         if (written != dlen)
177                 return SR_ERR_DATA;
178         return SR_OK;
179 }
180
181 /*
182  * Receive a response from the relay card. Assumes fixed size payload,
183  * considers short reads fatal.
184  */
185 static int recv_response(struct sr_serial_dev_inst *ser,
186         uint8_t *data, size_t dlen)
187 {
188         int ret;
189         size_t got;
190
191         ret = serial_read_blocking(ser, data, dlen, READ_TIMEOUT_MS);
192         if (ret < 0)
193                 return ret;
194         got = (size_t)ret;
195         if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
196                 GString *txt = sr_hexdump_new(data, got);
197                 sr_spew("<-- RX %s.", txt->str);
198                 sr_hexdump_free(txt);
199         }
200         if (got != dlen)
201                 return SR_ERR_DATA;
202         return SR_OK;
203 }
204
205 /* Send a request then receive a response. Convenience routine. */
206 static int send_then_recv(struct sr_serial_dev_inst *serial,
207         const uint8_t *tx_data, size_t tx_length,
208         uint8_t *rx_data, size_t rx_length)
209 {
210         int ret;
211
212         if (tx_data && tx_length) {
213                 ret = send_request(serial, tx_data, tx_length);
214                 if (ret != SR_OK)
215                         return ret;
216         }
217
218         if (rx_data && rx_length) {
219                 ret = recv_response(serial, rx_data, rx_length);
220                 if (ret != SR_OK)
221                         return ret;
222         }
223
224         return SR_OK;
225 }
226
227 /* Identify the relay card, gather version information details. */
228 SR_PRIV int devantech_eth008_get_model(struct sr_serial_dev_inst *serial,
229         uint8_t *model_code, uint8_t *hw_version, uint8_t *fw_version)
230 {
231         uint8_t req[1], *wrptr;
232         uint8_t rsp[3], v8;
233         const uint8_t *rdptr;
234         int ret;
235
236         if (model_code)
237                 *model_code = 0;
238         if (hw_version)
239                 *hw_version = 0;
240         if (fw_version)
241                 *fw_version = 0;
242
243         wrptr = req;
244         write_u8_inc(&wrptr, CMD_GET_MODULE_INFO);
245         ret = send_then_recv(serial, req, wrptr - req, rsp, sizeof(rsp));
246         if (ret != SR_OK)
247                 return ret;
248         rdptr = rsp;
249
250         v8 = read_u8_inc(&rdptr);
251         if (model_code)
252                 *model_code = v8;
253         v8 = read_u8_inc(&rdptr);
254         if (hw_version)
255                 *hw_version = v8;
256         v8 = read_u8_inc(&rdptr);
257         if (fw_version)
258                 *fw_version = v8;
259
260         return SR_OK;
261 }
262
263 /* Get the relay card's serial number (its MAC address). */
264 SR_PRIV int devantech_eth008_get_serno(struct sr_serial_dev_inst *serial,
265         char *text_buffer, size_t text_length)
266 {
267         uint8_t req[1], *wrptr;
268         uint8_t rsp[6], b;
269         const uint8_t *rdptr, *endptr;
270         size_t written;
271         int ret;
272
273         if (text_buffer && !text_length)
274                 return SR_ERR_ARG;
275         if (text_buffer)
276                 memset(text_buffer, 0, text_length);
277
278         wrptr = req;
279         write_u8_inc(&wrptr, CMD_GET_SERIAL_NUMBER);
280         ret = send_then_recv(serial, req, wrptr - req, rsp, sizeof(rsp));
281         if (ret != SR_OK)
282                 return ret;
283         rdptr = rsp;
284
285         endptr = rsp + sizeof(rsp);
286         while (rdptr < endptr && text_buffer && text_length >= 3) {
287                 b = read_u8_inc(&rdptr);
288                 written = snprintf(text_buffer, text_length, "%02x", b);
289                 text_buffer += written;
290                 text_length -= written;
291         }
292
293         return SR_OK;
294 }
295
296 /* Update an internal cache from the relay card's current state. */
297 SR_PRIV int devantech_eth008_cache_state(const struct sr_dev_inst *sdi)
298 {
299         struct sr_serial_dev_inst *serial;
300         struct dev_context *devc;
301         size_t rx_size;
302         uint8_t req[1], *wrptr;
303         uint8_t rsp[4];
304         const uint8_t *rdptr;
305         uint32_t have;
306         int ret;
307
308         serial = sdi->conn;
309         if (!serial)
310                 return SR_ERR_ARG;
311         devc = sdi->priv;
312         if (!devc)
313                 return SR_ERR_ARG;
314
315         /* Get the state of digital outputs when the model supports them. */
316         if (devc->model->ch_count_do) {
317                 rx_size = devc->model->width_do;
318                 if (rx_size > sizeof(rsp))
319                         return SR_ERR_NA;
320
321                 wrptr = req;
322                 write_u8_inc(&wrptr, CMD_DIGITAL_GET_OUTPUTS);
323                 ret = send_then_recv(serial, req, wrptr - req, rsp, rx_size);
324                 if (ret != SR_OK)
325                         return ret;
326                 rdptr = rsp;
327
328                 switch (rx_size) {
329                 case 1:
330                         have = read_u8_inc(&rdptr);
331                         break;
332                 case 2:
333                         have = read_u16le_inc(&rdptr);
334                         break;
335                 case 3:
336                         have = read_u24le_inc(&rdptr);
337                         break;
338                 default:
339                         return SR_ERR_NA;
340                 }
341                 have &= devc->mask_do;
342                 devc->curr_do = have;
343         }
344
345         /*
346          * Get the state of digital inputs when the model supports them.
347          * (Sending unsupported requests to unaware firmware versions
348          * yields no response. That's why requests must be conditional.)
349          *
350          * Caching the state of analog inputs is condidered undesirable.
351          * Firmware does conversion at the very moment when the request
352          * is received to get a voltage reading.
353          */
354         if (devc->model->ch_count_di) {
355                 rx_size = devc->model->width_di;
356                 if (rx_size > sizeof(rsp))
357                         return SR_ERR_NA;
358
359                 wrptr = req;
360                 write_u8_inc(&wrptr, CMD_DIGITAL_GET_INPUTS);
361                 ret = send_then_recv(serial, req, wrptr - req, rsp, rx_size);
362                 if (ret != SR_OK)
363                         return ret;
364                 rdptr = rsp;
365
366                 switch (rx_size) {
367                 case 2:
368                         have = read_u16be_inc(&rdptr);
369                         break;
370                 case 4:
371                         have = read_u32be_inc(&rdptr);
372                         break;
373                 default:
374                         return SR_ERR_NA;
375                 }
376                 have &= (1UL << devc->model->ch_count_di) - 1;
377                 devc->curr_di = have;
378         }
379
380         return SR_OK;
381 }
382
383 /* Query the state of an individual relay channel. */
384 SR_PRIV int devantech_eth008_query_do(const struct sr_dev_inst *sdi,
385         const struct sr_channel_group *cg, gboolean *on)
386 {
387         struct dev_context *devc;
388         struct channel_group_context *cgc;
389         uint32_t have;
390         int ret;
391
392         devc = sdi->priv;
393         if (!devc)
394                 return SR_ERR_ARG;
395
396         /* Unconditionally update the internal cache. */
397         ret = devantech_eth008_cache_state(sdi);
398         if (ret != SR_OK)
399                 return ret;
400
401         /*
402          * Only reject unexpected requests after the update. Get the
403          * individual channel's state from the cache of all channels.
404          */
405         if (!cg)
406                 return SR_ERR_ARG;
407         cgc = cg->priv;
408         if (!cgc)
409                 return SR_ERR_BUG;
410         if (cgc->index >= devc->model->ch_count_do)
411                 return SR_ERR_ARG;
412         have = devc->curr_do;
413         have >>= cgc->index;
414         have &= 1 << 0;
415         if (on)
416                 *on = have ? TRUE : FALSE;
417
418         return SR_OK;
419 }
420
421 /*
422  * Manipulate the state of an individual relay channel (when cg is given).
423  * Or set/clear all channels at the same time (when cg is NULL).
424  */
425 SR_PRIV int devantech_eth008_setup_do(const struct sr_dev_inst *sdi,
426         const struct sr_channel_group *cg, gboolean on)
427 {
428         struct sr_serial_dev_inst *serial;
429         struct dev_context *devc;
430         size_t width_do;
431         struct channel_group_context *cgc;
432         size_t number;
433         uint32_t reg;
434         uint8_t req[4], *wrptr, cmd;
435         uint8_t rsp[1], v8;
436         const uint8_t *rdptr;
437         int ret;
438
439         serial = sdi->conn;
440         if (!serial)
441                 return SR_ERR_ARG;
442         devc = sdi->priv;
443         if (!devc)
444                 return SR_ERR_ARG;
445         cgc = cg ? cg->priv : NULL;
446         if (cgc && cgc->index >= devc->model->ch_count_do)
447                 return SR_ERR_ARG;
448
449         width_do = devc->model->width_do;
450         if (1 + width_do > sizeof(req))
451                 return SR_ERR_NA;
452
453         wrptr = req;
454         if (cgc) {
455                 /* Manipulate an individual channel. */
456                 cmd = on ? CMD_DIGITAL_ACTIVE : CMD_DIGITAL_INACTIVE;
457                 number = cgc->number;
458                 write_u8_inc(&wrptr, cmd);
459                 write_u8_inc(&wrptr, number & 0xff);
460                 write_u8_inc(&wrptr, 0); /* Just set/clear, no pulse. */
461         } else {
462                 /* Manipulate all channels at the same time. */
463                 reg = on ? devc->mask_do : 0;
464                 write_u8_inc(&wrptr, CMD_DIGITAL_SET_OUTPUTS);
465                 switch (width_do) {
466                 case 1:
467                         write_u8_inc(&wrptr, reg & 0xff);
468                         break;
469                 case 2:
470                         write_u16le_inc(&wrptr, reg & 0xffff);
471                         break;
472                 case 3:
473                         write_u24le_inc(&wrptr, reg & 0xffffff);
474                         break;
475                 default:
476                         return SR_ERR_NA;
477                 }
478         }
479         ret = send_then_recv(serial, req, wrptr - req, rsp, sizeof(rsp));
480         if (ret != SR_OK)
481                 return ret;
482         rdptr = rsp;
483
484         v8 = read_u8_inc(&rdptr);
485         if (v8 != 0)
486                 return SR_ERR_DATA;
487
488         return SR_OK;
489 }
490
491 SR_PRIV int devantech_eth008_query_di(const struct sr_dev_inst *sdi,
492         const struct sr_channel_group *cg, gboolean *on)
493 {
494         struct dev_context *devc;
495         struct channel_group_context *cgc;
496         uint32_t have;
497         int ret;
498
499         /* Unconditionally update the internal cache. */
500         ret = devantech_eth008_cache_state(sdi);
501         if (ret != SR_OK)
502                 return ret;
503
504         /*
505          * Only reject unexpected requests after the update. Get the
506          * individual channel's state from the cache of all channels.
507          */
508         devc = sdi->priv;
509         if (!devc)
510                 return SR_ERR_ARG;
511         if (!cg)
512                 return SR_ERR_ARG;
513         cgc = cg->priv;
514         if (!cgc)
515                 return SR_ERR_BUG;
516         if (cgc->index >= devc->model->ch_count_di)
517                 return SR_ERR_ARG;
518         have = devc->curr_di;
519         have >>= cgc->index;
520         have &= 1 << 0;
521         if (on)
522                 *on = have ? TRUE : FALSE;
523
524         return SR_OK;
525 }
526
527 SR_PRIV int devantech_eth008_query_ai(const struct sr_dev_inst *sdi,
528         const struct sr_channel_group *cg, uint16_t *adc_value)
529 {
530         struct sr_serial_dev_inst *serial;
531         struct dev_context *devc;
532         struct channel_group_context *cgc;
533         uint8_t req[2], *wrptr;
534         uint8_t rsp[2];
535         const uint8_t *rdptr;
536         uint32_t have;
537         int ret;
538
539         serial = sdi->conn;
540         if (!serial)
541                 return SR_ERR_ARG;
542         devc = sdi->priv;
543         if (!devc)
544                 return SR_ERR_ARG;
545         if (!cg)
546                 return SR_ERR_ARG;
547         cgc = cg->priv;
548         if (!cgc)
549                 return SR_ERR_ARG;
550         if (cgc->index >= devc->model->ch_count_ai)
551                 return SR_ERR_ARG;
552
553         wrptr = req;
554         write_u8_inc(&wrptr, CMD_ANALOG_GET_INPUT);
555         write_u8_inc(&wrptr, cgc->number & 0xff);
556         ret = send_then_recv(serial, req, wrptr - req, rsp, sizeof(rsp));
557         if (ret != SR_OK)
558                 return ret;
559         rdptr = rsp;
560
561         /*
562          * The interpretation of analog readings differs across models.
563          * All firmware versions provide an ADC result in BE format in
564          * a 16bit response. Some models provide 10 significant digits,
565          * others provide 12 bits. Full scale can either be 3V3 or 5V0.
566          * Some devices are 5V tolerant but won't read more than 3V3
567          * values (and clip above that full scale value). Some firmware
568          * versions support request 0x33 in addition to 0x32.
569          *
570          * This is why this implementation provides the result to the
571          * caller as a unit-less value. It is also what the firmware's
572          * web interface does.
573          */
574         have = read_u16be_inc(&rdptr);
575         if (adc_value)
576                 *adc_value = have;
577
578         return SR_OK;
579 }
580
581 SR_PRIV int devantech_eth008_query_supply(const struct sr_dev_inst *sdi,
582         const struct sr_channel_group *cg, uint16_t *millivolts)
583 {
584         struct sr_serial_dev_inst *serial;
585         uint8_t req[1], *wrptr;
586         uint8_t rsp[1];
587         const uint8_t *rdptr;
588         uint16_t have;
589         int ret;
590
591         (void)cg;
592
593         serial = sdi->conn;
594         if (!serial)
595                 return SR_ERR_ARG;
596
597         wrptr = req;
598         write_u8_inc(&wrptr, CMD_GET_SUPPLY_VOLTS);
599         ret = send_then_recv(serial, req, wrptr - req, rsp, sizeof(rsp));
600         if (ret != SR_OK)
601                 return ret;
602         rdptr = rsp;
603
604         /* Gets a byte for voltage in units of 0.1V. Scale up to mV. */
605         have = read_u8_inc(&rdptr);
606         have *= 100;
607         if (millivolts)
608                 *millivolts = have;
609
610         return SR_OK;
611 }