ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sdi12.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sdi12.h
6 * @brief SDI-12 sensor-bus command / response codec (PROTOCORE_ENABLE_SDI12).
7 *
8 * SDI-12 is the 1200-baud single-wire ASCII bus used by environmental / agricultural sensors
9 * (soil moisture, water level, weather). A recorder addresses a sensor by a single character
10 * (0-9, A-Z, a-z) and sends `<addr><command>!`; the sensor replies `<addr>...<CR><LF>`. This
11 * codec builds the standard commands, parses the measurement response (`atttn`: seconds until
12 * ready + value count), splits the data values, and does the SDI-12 CRC (the `aMC!` / `aCC!`
13 * CRC-protected variants).
14 *
15 * The wire is a single 1200-baud 7E1 line with a 5 V break / marking convention; on an ESP32
16 * it needs a small level / direction circuit, and the UART transport is the application's.
17 * Pure codec, host-tested. Bridge a sensor string onto Wi-Fi by polling `aM!` / `aD0!` and
18 * publishing the values.
19 *
20 * @author Douglas Quigg (dstroy0)
21 * @date 2026
22 */
23
24#ifndef PROTOCORE_SDI12_H
25#define PROTOCORE_SDI12_H
26
27#include "protocore_config.h" // the entry point: protocore_types.h for the widths
28
29#if PROTOCORE_ENABLE_SDI12
30
32
33// This module holds nothing between calls, so it carves no borrow and states none. An entry
34// takes one all the same, and never reads it, so every namespace in the tree is invoked the
35// same way.
36
37#define SDI12_CRC_POLY 0xA001u ///< CRC-16 polynomial (reflected 0x8005), init 0x0000
38
39#define SDI12_CRC_CHARS 3 ///< the CRC is appended as 3 printable ASCII octets
40
41/** @brief A decoded identify (aI!) response. Each field is fixed-width per the SDI-12 spec and
42 * NUL-terminated (the spec pads short values with spaces, which are left in place). */
43typedef struct
44{
45 char addr; ///< sensor address
46 char sdi_version[3]; ///< SDI-12 version "ll" (e.g. "14" = version 1.4) + NUL
47 char vendor[9]; ///< 8-character vendor identification + NUL
48 char model[7]; ///< 6-character sensor model number + NUL
49 char sensor_version[4]; ///< 3-character sensor version + NUL
50} Sdi12Identity;
51
52/** @brief What build takes: buf, cap, addr, body. */
53typedef struct
54{
55 char *buf;
56 size_t cap;
57 char addr;
58 const char *body;
59} Sdi12BuildArgs;
60
61/** @brief What build_ack takes: buf, cap, addr. */
62typedef struct
63{
64 char *buf;
65 size_t cap;
66 char addr;
67} Sdi12BuildAckArgs;
68
69/** @brief What build_identify takes: buf, cap, addr. */
70typedef struct
71{
72 char *buf;
73 size_t cap;
74 char addr;
75} Sdi12BuildIdentifyArgs;
76
77/** @brief What build_measure takes: buf, cap, addr, with_crc. */
78typedef struct
79{
80 char *buf;
81 size_t cap;
82 char addr;
83 proto_bool with_crc;
84} Sdi12BuildMeasureArgs;
85
86/** @brief What build_concurrent takes: buf, cap, addr, with_crc. */
87typedef struct
88{
89 char *buf;
90 size_t cap;
91 char addr;
92 proto_bool with_crc;
93} Sdi12BuildConcurrentArgs;
94
95/** @brief What build_measure_additional takes: buf, cap, addr, ... */
96typedef struct
97{
98 char *buf;
99 size_t cap;
100 char addr;
101 uint8_t m_index;
102 proto_bool with_crc;
103} Sdi12BuildMeasureAdditionalArgs;
104
105/** @brief What build_concurrent_additional takes: buf, cap, addr, ... */
106typedef struct
107{
108 char *buf;
109 size_t cap;
110 char addr;
111 uint8_t c_index;
112 proto_bool with_crc;
113} Sdi12BuildConcurrentAdditionalArgs;
114
115/** @brief What build_continuous takes: buf, cap, addr, r_index, ... */
116typedef struct
117{
118 char *buf;
119 size_t cap;
120 char addr;
121 uint8_t r_index;
122 proto_bool with_crc;
123} Sdi12BuildContinuousArgs;
124
125/** @brief What build_verify takes: buf, cap, addr. */
126typedef struct
127{
128 char *buf;
129 size_t cap;
130 char addr;
131} Sdi12BuildVerifyArgs;
132
133/** @brief What build_data takes: buf, cap, addr, d_index. */
134typedef struct
135{
136 char *buf;
137 size_t cap;
138 char addr;
139 uint8_t d_index;
140} Sdi12BuildDataArgs;
141
142/** @brief What build_change_address takes: buf, cap, addr, new_addr. */
143typedef struct
144{
145 char *buf;
146 size_t cap;
147 char addr;
148 char new_addr;
149} Sdi12BuildChangeAddressArgs;
150
151/** @brief What build_query_address takes: buf, cap. */
152typedef struct
153{
154 char *buf;
155 size_t cap;
156} Sdi12BuildQueryAddressArgs;
157
158/** @brief What parse_measure takes: resp, len, addr, ready_sec, ... */
159typedef struct
160{
161 const char *resp;
162 size_t len;
163 char *addr;
164 uint16_t *ready_sec;
165 uint8_t *num_values;
166} Sdi12ParseMeasureArgs;
167
168/** @brief What parse_values takes: resp, len, out, max, n. */
169typedef struct
170{
171 const char *resp;
172 size_t len;
173 float *out;
174 size_t max;
175 size_t *n;
176} Sdi12ParseValuesArgs;
177
178/** @brief What parse_identify takes: resp, len, out. */
179typedef struct
180{
181 const char *resp;
182 size_t len;
183 Sdi12Identity *out;
184} Sdi12ParseIdentifyArgs;
185
186/** @brief What crc16 takes: data, len. */
187typedef struct
188{
189 const uint8_t *data;
190 size_t len;
191} Sdi12Crc16Args;
192
193/** @brief What crc_encode takes: crc, out. */
194typedef struct
195{
196 uint16_t crc;
197 char *out; ///< SDI12_CRC_CHARS bytes.
198} Sdi12CrcEncodeArgs;
199
200/** @brief What check_crc takes: resp, len. */
201typedef struct
202{
203 const char *resp;
204 size_t len;
205} Sdi12CheckCrcArgs;
206
207/**
208 * @brief SDI-12 sensor-bus command / response codec (PROTOCORE_ENABLE_SDI12). SDI-12 is the 1200-baud single-wire ...
209 *
210 * A caller sets the members a call takes, invokes it through ::Sdi12 with the bytes it runs
211 * out of, and reads the outcome off the same handle.
212 *
213 * Sdi12.build_args.buf = ...;
214 * Sdi12.build_args.cap = ...;
215 * Sdi12.build_args.addr = ...;
216 * Sdi12.build_args.body = ...;
217 * Sdi12.build(work);
218 * // Sdi12.n is what the call reports
219 *
220 * @var Sdi12Ns::build_args what build takes: buf, cap, addr, body
221 * @var Sdi12Ns::build_ack_args what build_ack takes: buf, cap, addr
222 * @var Sdi12Ns::build_identify_args what build_identify takes: buf, cap, addr
223 * @var Sdi12Ns::build_measure_args what build_measure takes: buf, cap, addr, with_crc
224 * @var Sdi12Ns::build_concurrent_args what build_concurrent takes: buf, cap, addr, with_crc
225 * @var Sdi12Ns::build_measure_additional_args what build_measure_additional takes: buf, cap, addr,
226 * @var Sdi12Ns::build_concurrent_additional_args what build_concurrent_additional takes: buf, cap, addr,
227 * @var Sdi12Ns::build_continuous_args what build_continuous takes: buf, cap, addr, r_index,
228 * @var Sdi12Ns::build_verify_args what build_verify takes: buf, cap, addr
229 * @var Sdi12Ns::build_data_args what build_data takes: buf, cap, addr, d_index
230 * @var Sdi12Ns::build_change_address_args what build_change_address takes: buf, cap, addr, new_addr
231 * @var Sdi12Ns::build_query_address_args what build_query_address takes: buf, cap
232 * @var Sdi12Ns::parse_measure_args what parse_measure takes: resp, len, addr, ready_sec,
233 * @var Sdi12Ns::parse_values_args what parse_values takes: resp, len, out, max, n
234 * @var Sdi12Ns::parse_identify_args what parse_identify takes: resp, len, out
235 * @var Sdi12Ns::crc16_args what crc16 takes: data, len
236 * @var Sdi12Ns::crc_encode_args what crc_encode takes: crc, out
237 * @var Sdi12Ns::check_crc_args what check_crc takes: resp, len
238 * @var Sdi12Ns::ok true iff len covers the 20 fixed octets; false otherwise
239 * @var Sdi12Ns::n the count a call reports
240 * @var Sdi12Ns::crc what a call reports
241 * @var Sdi12Ns::build generic `<addr><body>!` command (body is the command letters, e.g. ...
242 * @var Sdi12Ns::build_ack acknowledge-active command `a!`
243 * @var Sdi12Ns::build_identify send-identification command `aI!`
244 * @var Sdi12Ns::build_measure start-measurement command `aM!` (or `aMC!` when with_crc)
245 * @var Sdi12Ns::build_concurrent concurrent-measurement command `aC!` (or `aCC!` when with_crc)
246 * @var Sdi12Ns::build_measure_additional additional-measurement command `aM<n>!` (or `aMC<n>!` when ...
247 * @var Sdi12Ns::build_concurrent_additional additional-concurrent command `aC<n>!` (or `aCC<n>!` when ...
248 * @var Sdi12Ns::build_continuous continuous-measurement command `aR<n>!` (or `aRC<n>!` when ...
249 * @var Sdi12Ns::build_verify start-verification command `aV!`; the response uses the same ...
250 * @var Sdi12Ns::build_data send-data command `aD<n>!` (d_index 0..9)
251 * @var Sdi12Ns::build_change_address change-address command `aA<b>!` (new_addr is the new sensor address)
252 * @var Sdi12Ns::build_query_address address-query command `?!` (asks the single connected sensor for ...
253 * @var Sdi12Ns::parse_measure parse a measurement response `atttn<CR><LF>`: ready_sec = seconds ...
254 * @var Sdi12Ns::parse_values split a data response `a<+/-value...><CR><LF>` into floats. Skips ...
255 * @var Sdi12Ns::parse_identify parse an identify (aI!) response: address + 2-char SDI-12 version + ...
256 * @var Sdi12Ns::crc16 compute the SDI-12 CRC-16 over data
257 * @var Sdi12Ns::crc_encode encode a CRC into its 3 printable ASCII octets (out[0..2])
258 * @var Sdi12Ns::check_crc verify a CRC-protected response: the 3 octets before the trailing ...
259 *
260 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
261 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
262 * a caller drives every namespace the same way.
263 */
264typedef struct
265{
266 Sdi12BuildArgs build_args;
267 Sdi12BuildAckArgs build_ack_args;
268 Sdi12BuildIdentifyArgs build_identify_args;
269 Sdi12BuildMeasureArgs build_measure_args;
270 Sdi12BuildConcurrentArgs build_concurrent_args;
271 Sdi12BuildMeasureAdditionalArgs build_measure_additional_args;
272 Sdi12BuildConcurrentAdditionalArgs build_concurrent_additional_args;
273 Sdi12BuildContinuousArgs build_continuous_args;
274 Sdi12BuildVerifyArgs build_verify_args;
275 Sdi12BuildDataArgs build_data_args;
276 Sdi12BuildChangeAddressArgs build_change_address_args;
277 Sdi12BuildQueryAddressArgs build_query_address_args;
278 Sdi12ParseMeasureArgs parse_measure_args;
279 Sdi12ParseValuesArgs parse_values_args;
280 Sdi12ParseIdentifyArgs parse_identify_args;
281 Sdi12Crc16Args crc16_args;
282 Sdi12CrcEncodeArgs crc_encode_args;
283 Sdi12CheckCrcArgs check_crc_args;
284 proto_bool ok;
285 size_t n;
286 uint16_t crc;
287} Sdi12Vars;
288
289/** @brief The operands and the outcome. */
290extern Sdi12Vars Sdi12V;
291
292/** @brief The entries. */
293typedef struct
294{
295 void (*const build)(uint8_t *work);
296 void (*const build_ack)(uint8_t *work);
297 void (*const build_identify)(uint8_t *work);
298 void (*const build_measure)(uint8_t *work);
299 void (*const build_concurrent)(uint8_t *work);
300 void (*const build_measure_additional)(uint8_t *work);
301 void (*const build_concurrent_additional)(uint8_t *work);
302 void (*const build_continuous)(uint8_t *work);
303 void (*const build_verify)(uint8_t *work);
304 void (*const build_data)(uint8_t *work);
305 void (*const build_change_address)(uint8_t *work);
306 void (*const build_query_address)(uint8_t *work);
307 void (*const parse_measure)(uint8_t *work);
308 void (*const parse_values)(uint8_t *work);
309 void (*const parse_identify)(uint8_t *work);
310 void (*const crc16)(uint8_t *work);
311 void (*const crc_encode)(uint8_t *work);
312 void (*const check_crc)(uint8_t *work);
313} Sdi12Ns;
314
315// What the table binds, defined once in the .c and taking one parameter each: everything
316// else an entry needs is an operand in Sdi12V or a region of the borrow at a fixed offset.
317void protocore_sdi12_build(uint8_t *work);
318void protocore_sdi12_build_ack(uint8_t *work);
319void protocore_sdi12_build_identify(uint8_t *work);
320void protocore_sdi12_build_measure(uint8_t *work);
321void protocore_sdi12_build_concurrent(uint8_t *work);
322void protocore_sdi12_build_measure_additional(uint8_t *work);
323void protocore_sdi12_build_concurrent_additional(uint8_t *work);
324void protocore_sdi12_build_continuous(uint8_t *work);
325void protocore_sdi12_build_verify(uint8_t *work);
326void protocore_sdi12_build_data(uint8_t *work);
327void protocore_sdi12_build_change_address(uint8_t *work);
328void protocore_sdi12_build_query_address(uint8_t *work);
329void protocore_sdi12_parse_measure(uint8_t *work);
330void protocore_sdi12_parse_values(uint8_t *work);
331void protocore_sdi12_parse_identify(uint8_t *work);
332void protocore_sdi12_crc16(uint8_t *work);
333void protocore_sdi12_crc_encode(uint8_t *work);
334void protocore_sdi12_check_crc(uint8_t *work);
335
336// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
337// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
338// `Sdi12.build(work)` resolves to a named function and becomes a DIRECT call. An extern table
339// leaves the call indirect and the symbol live at every level, -O2 -flto included.
340static const Sdi12Ns Sdi12 __attribute__((unused)) = {
341 .build = protocore_sdi12_build,
342 .build_ack = protocore_sdi12_build_ack,
343 .build_identify = protocore_sdi12_build_identify,
344 .build_measure = protocore_sdi12_build_measure,
345 .build_concurrent = protocore_sdi12_build_concurrent,
346 .build_measure_additional = protocore_sdi12_build_measure_additional,
347 .build_concurrent_additional = protocore_sdi12_build_concurrent_additional,
348 .build_continuous = protocore_sdi12_build_continuous,
349 .build_verify = protocore_sdi12_build_verify,
350 .build_data = protocore_sdi12_build_data,
351 .build_change_address = protocore_sdi12_build_change_address,
352 .build_query_address = protocore_sdi12_build_query_address,
353 .parse_measure = protocore_sdi12_parse_measure,
354 .parse_values = protocore_sdi12_parse_values,
355 .parse_identify = protocore_sdi12_parse_identify,
356 .crc16 = protocore_sdi12_crc16,
357 .crc_encode = protocore_sdi12_crc_encode,
358 .check_crc = protocore_sdi12_check_crc,
359};
360
362
363#endif // PROTOCORE_ENABLE_SDI12
364
365#endif // PROTOCORE_SDI12_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97