ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sdi12.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sdi12.cpp
6 * @brief SDI-12 command / response codec (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_SDI12
12
13#include "shared_primitives/crc.h" // PC_CRC16_ARC
15#include <string.h>
16
17size_t pc_sdi12_build(char *buf, size_t cap, char addr, const char *body)
18{
19 if (!buf || !body)
20 {
21 return 0;
22 }
23 size_t blen = strnlen(body, cap);
24 size_t n = 1 + blen + 1; // addr + body + '!'
25 if (cap < n + 1) // + room for the NUL terminator
26 {
27 return 0;
28 }
29 buf[0] = addr;
30 memcpy(buf + 1, body, blen);
31 buf[1 + blen] = '!';
32 buf[n] = '\0';
33 return n;
34}
35
36size_t pc_sdi12_build_ack(char *buf, size_t cap, char addr)
37{
38 return pc_sdi12_build(buf, cap, addr, "");
39}
40
41size_t pc_sdi12_build_identify(char *buf, size_t cap, char addr)
42{
43 return pc_sdi12_build(buf, cap, addr, "I");
44}
45
46size_t pc_sdi12_build_measure(char *buf, size_t cap, char addr, bool with_crc)
47{
48 return pc_sdi12_build(buf, cap, addr, with_crc ? "MC" : "M");
49}
50
51size_t pc_sdi12_build_concurrent(char *buf, size_t cap, char addr, bool with_crc)
52{
53 return pc_sdi12_build(buf, cap, addr, with_crc ? "CC" : "C");
54}
55
56size_t pc_sdi12_build_measure_additional(char *buf, size_t cap, char addr, uint8_t m_index, bool with_crc)
57{
58 if (m_index < 1 || m_index > 9)
59 {
60 return 0;
61 }
62 char body[4];
63 size_t b = 0;
64 body[b++] = 'M';
65 if (with_crc)
66 {
67 body[b++] = 'C';
68 }
69 body[b++] = (char)('0' + m_index);
70 body[b] = '\0';
71 return pc_sdi12_build(buf, cap, addr, body);
72}
73
74size_t pc_sdi12_build_concurrent_additional(char *buf, size_t cap, char addr, uint8_t c_index, bool with_crc)
75{
76 if (c_index < 1 || c_index > 9)
77 {
78 return 0;
79 }
80 char body[4];
81 size_t b = 0;
82 body[b++] = 'C';
83 if (with_crc)
84 {
85 body[b++] = 'C';
86 }
87 body[b++] = (char)('0' + c_index);
88 body[b] = '\0';
89 return pc_sdi12_build(buf, cap, addr, body);
90}
91
92size_t pc_sdi12_build_continuous(char *buf, size_t cap, char addr, uint8_t r_index, bool with_crc)
93{
94 if (r_index > 9)
95 {
96 return 0;
97 }
98 char body[4];
99 size_t b = 0;
100 body[b++] = 'R';
101 if (with_crc)
102 {
103 body[b++] = 'C';
104 }
105 body[b++] = (char)('0' + r_index);
106 body[b] = '\0';
107 return pc_sdi12_build(buf, cap, addr, body);
108}
109
110size_t pc_sdi12_build_verify(char *buf, size_t cap, char addr)
111{
112 return pc_sdi12_build(buf, cap, addr, "V");
113}
114
115size_t pc_sdi12_build_data(char *buf, size_t cap, char addr, uint8_t d_index)
116{
117 if (d_index > 9)
118 {
119 return 0;
120 }
121 char body[3] = {'D', (char)('0' + d_index), '\0'};
122 return pc_sdi12_build(buf, cap, addr, body);
123}
124
125size_t pc_sdi12_build_change_address(char *buf, size_t cap, char addr, char new_addr)
126{
127 char body[3] = {'A', new_addr, '\0'};
128 return pc_sdi12_build(buf, cap, addr, body);
129}
130
131size_t pc_sdi12_build_query_address(char *buf, size_t cap)
132{
133 return pc_sdi12_build(buf, cap, '?', "");
134}
135
136bool pc_sdi12_parse_measure(const char *resp, size_t len, char *addr, uint16_t *ready_sec, uint8_t *num_values)
137{
138 if (!resp || len < 5) // a<ttt><n> is at least 5 octets
139 {
140 return false;
141 }
142 for (int i = 1; i <= 3; i++)
143 {
144 if (!pc_np_digit(resp[i]))
145 {
146 return false;
147 }
148 }
149 if (!pc_np_digit(resp[4]))
150 {
151 return false;
152 }
153 if (addr)
154 {
155 *addr = resp[0];
156 }
157 if (ready_sec)
158 {
159 *ready_sec = (uint16_t)((resp[1] - '0') * 100 + (resp[2] - '0') * 10 + (resp[3] - '0'));
160 }
161 // The value count is the remaining digits (1 digit for aM!, 2 for aC!).
162 uint16_t count = 0;
163 for (size_t i = 4; i < len && pc_np_digit(resp[i]); i++)
164 {
165 count = (uint16_t)(count * 10 + (resp[i] - '0'));
166 }
167 if (num_values)
168 {
169 *num_values = (uint8_t)count;
170 }
171 return true;
172}
173
174bool pc_sdi12_parse_values(const char *resp, size_t len, float *out, size_t max, size_t *n)
175{
176 if (!resp || !out || !n)
177 {
178 return false;
179 }
180 size_t cnt = 0;
181 size_t i = 1; // skip the leading address
182 while (i < len && cnt < max)
183 {
184 char c = resp[i];
185 if (c == '\r' || c == '\n')
186 {
187 break;
188 }
189 if (c == '+' || c == '-')
190 {
191 const char *start = resp + i;
192 const char *end = start;
193 // pc_strtof handles a leading '-'; for '+' parse the magnitude after the sign.
194 float v = (c == '+') ? pc_strtof(start + 1, &end) : pc_strtof(start, &end);
195 if (end == start || (c == '+' && end == start + 1)) // no digits consumed
196 {
197 i++;
198 continue;
199 }
200 out[cnt++] = v;
201 i = (size_t)(end - resp);
202 }
203 else
204 {
205 i++; // CRC octets / separators are skipped (they never begin with +/-)
206 }
207 }
208 *n = cnt;
209 return true;
210}
211
212bool pc_sdi12_parse_identify(const char *resp, size_t len, Sdi12Identity *out)
213{
214 if (!resp || !out || len < 20) // addr(1) + version(2) + vendor(8) + model(6) + sensor version(3)
215 {
216 return false;
217 }
218 out->addr = resp[0];
219 memcpy(out->sdi_version, resp + 1, 2);
220 out->sdi_version[2] = '\0';
221 memcpy(out->vendor, resp + 3, 8);
222 out->vendor[8] = '\0';
223 memcpy(out->model, resp + 11, 6);
224 out->model[6] = '\0';
225 memcpy(out->sensor_version, resp + 17, 3);
226 out->sensor_version[3] = '\0';
227 return true;
228}
229
230uint16_t pc_sdi12_crc16(const uint8_t *data, size_t len)
231{
232 // SDI-12 v1.3 uses the reflected CRC-16 (SDI12_CRC_POLY = 0xA001 = reflect(0x8005), init 0, no
233 // final XOR) - cataloged as CRC-16/ARC. test_crc diffs the shared engine against the loop that
234 // used to live here over every length 0..64, so this is byte-identical to it.
235 return (uint16_t)pc_crc(&PC_CRC16_ARC, data, len);
236}
237
238void pc_sdi12_crc_encode(uint16_t crc, char out[SDI12_CRC_CHARS])
239{
240 out[0] = (char)(0x40u | (crc >> 12)); // top bits
241 out[1] = (char)(0x40u | ((crc >> 6) & 0x3Fu));
242 out[2] = (char)(0x40u | (crc & 0x3Fu));
243}
244
245bool pc_sdi12_check_crc(const char *resp, size_t len)
246{
247 if (!resp)
248 {
249 return false;
250 }
251 // Trim a trailing <CR><LF> if present.
252 while (len > 0 && (resp[len - 1] == '\n' || resp[len - 1] == '\r'))
253 {
254 len--;
255 }
256 if (len < SDI12_CRC_CHARS + 1) // need at least 1 data octet + the 3 CRC octets
257 {
258 return false;
259 }
260 size_t data_len = len - SDI12_CRC_CHARS;
261 char enc[SDI12_CRC_CHARS];
262 pc_sdi12_crc_encode(pc_sdi12_crc16((const uint8_t *)resp, data_len), enc);
263 return memcmp(enc, resp + data_len, SDI12_CRC_CHARS) == 0;
264}
265
266#endif // PC_ENABLE_SDI12
Parameterized CRC engine - one source of truth for every cyclic redundancy check.
constexpr pc_crc_params PC_CRC16_ARC
CRC-16/ARC (a.k.a. CRC-16, IBM). check = 0xBB3D.
Definition crc.h:180
uint32_t pc_crc(const pc_crc_params *p, const uint8_t *data, size_t len)
One-shot CRC of len octets at data.
Definition crc.h:158
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
float pc_strtof(const char *s, const char **end)
Parse a float (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:154
bool pc_np_digit(char c)
Definition numparse.h:26
SDI-12 sensor-bus command / response codec (PC_ENABLE_SDI12).