ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sdi12.h
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.h
6 * @brief SDI-12 sensor-bus command / response codec (PC_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"
28
29#if PC_ENABLE_SDI12
30
31#include <stddef.h>
32#include <stdint.h>
33
34#define SDI12_CRC_POLY 0xA001u ///< CRC-16 polynomial (reflected 0x8005), init 0x0000
35#define SDI12_CRC_CHARS 3 ///< the CRC is appended as 3 printable ASCII octets
36
37// --- command builders: write a NUL-terminated command into @p buf, return its length or 0 ---
38
39/** @brief Generic `<addr><body>!` command (body is the command letters, e.g. "M" or "D0"). */
40size_t pc_sdi12_build(char *buf, size_t cap, char addr, const char *body);
41
42/** @brief Acknowledge-active command `a!`. */
43size_t pc_sdi12_build_ack(char *buf, size_t cap, char addr);
44
45/** @brief Send-identification command `aI!`. */
46size_t pc_sdi12_build_identify(char *buf, size_t cap, char addr);
47
48/** @brief Start-measurement command `aM!` (or `aMC!` when @p with_crc). */
49size_t pc_sdi12_build_measure(char *buf, size_t cap, char addr, bool with_crc);
50
51/** @brief Concurrent-measurement command `aC!` (or `aCC!` when @p with_crc). */
52size_t pc_sdi12_build_concurrent(char *buf, size_t cap, char addr, bool with_crc);
53
54/**
55 * @brief Additional-measurement command `aM<n>!` (or `aMC<n>!` when @p with_crc), @p m_index 1..9 - the
56 * secondary measurement sets a multi-parameter sensor exposes beyond the primary `aM!`. @return 0 for
57 * an @p m_index outside 1..9.
58 */
59size_t pc_sdi12_build_measure_additional(char *buf, size_t cap, char addr, uint8_t m_index, bool with_crc);
60
61/**
62 * @brief Additional-concurrent command `aC<n>!` (or `aCC<n>!` when @p with_crc), @p c_index 1..9 - the
63 * concurrent-mode counterpart of the additional measurement. @return 0 for a @p c_index outside 1..9.
64 */
65size_t pc_sdi12_build_concurrent_additional(char *buf, size_t cap, char addr, uint8_t c_index, bool with_crc);
66
67/**
68 * @brief Continuous-measurement command `aR<n>!` (or `aRC<n>!` when @p with_crc), @p r_index 0..9. Unlike
69 * `aM!`, a continuous-measurement sensor returns its values immediately (no service-request delay).
70 */
71size_t pc_sdi12_build_continuous(char *buf, size_t cap, char addr, uint8_t r_index, bool with_crc);
72
73/** @brief Start-verification command `aV!`; the response uses the same `atttn` timing form as `aM!`. */
74size_t pc_sdi12_build_verify(char *buf, size_t cap, char addr);
75
76/** @brief Send-data command `aD<n>!` (@p d_index 0..9). */
77size_t pc_sdi12_build_data(char *buf, size_t cap, char addr, uint8_t d_index);
78
79/** @brief Change-address command `aA<b>!` (@p new_addr is the new sensor address). */
80size_t pc_sdi12_build_change_address(char *buf, size_t cap, char addr, char new_addr);
81
82/** @brief Address-query command `?!` (asks the single connected sensor for its address). */
83size_t pc_sdi12_build_query_address(char *buf, size_t cap);
84
85// --- response parsing ---
86
87/**
88 * @brief Parse a measurement response `atttn<CR><LF>`: @p ready_sec = seconds until the data is
89 * ready, @p num_values = how many values will be available. Works for both the 1-digit (`aM!`)
90 * and 2-digit (`aC!`) value-count forms. @p addr (optional) receives the echoed address.
91 */
92bool pc_sdi12_parse_measure(const char *resp, size_t len, char *addr, uint16_t *ready_sec, uint8_t *num_values);
93
94/**
95 * @brief Split a data response `a<+/-value...><CR><LF>` into floats. Skips the leading address,
96 * decodes each sign-prefixed number, and stops at CR/LF or the buffer end. Returns the count
97 * via @p n (capped at @p max).
98 */
99bool pc_sdi12_parse_values(const char *resp, size_t len, float *out, size_t max, size_t *n);
100
101/** @brief A decoded identify (aI!) response. Each field is fixed-width per the SDI-12 spec and
102 * NUL-terminated (the spec pads short values with spaces, which are left in place). */
103struct Sdi12Identity
104{
105 char addr; ///< sensor address
106 char sdi_version[3]; ///< SDI-12 version "ll" (e.g. "14" = version 1.4) + NUL
107 char vendor[9]; ///< 8-character vendor identification + NUL
108 char model[7]; ///< 6-character sensor model number + NUL
109 char sensor_version[4]; ///< 3-character sensor version + NUL
110};
111
112/**
113 * @brief Parse an identify (aI!) response: address + 2-char SDI-12 version + 8-char vendor + 6-char model +
114 * 3-char sensor version (any optional field and the CRC / CR-LF that follow are ignored).
115 * @return true iff @p len covers the 20 fixed octets; false otherwise.
116 */
117bool pc_sdi12_parse_identify(const char *resp, size_t len, Sdi12Identity *out);
118
119// --- CRC (RFC-free SDI-12 16-bit CRC) ---
120
121/** @brief Compute the SDI-12 CRC-16 over @p data. */
122uint16_t pc_sdi12_crc16(const uint8_t *data, size_t len);
123
124/** @brief Encode a CRC into its 3 printable ASCII octets (out[0..2]). */
125void pc_sdi12_crc_encode(uint16_t crc, char out[SDI12_CRC_CHARS]);
126
127/**
128 * @brief Verify a CRC-protected response: the 3 octets before the trailing `<CR><LF>` must be
129 * the CRC of everything before them. @p len should include the data and the 3 CRC octets (the
130 * `<CR><LF>` may or may not be included).
131 */
132bool pc_sdi12_check_crc(const char *resp, size_t len);
133
134#endif // PC_ENABLE_SDI12
135#endif // PROTOCORE_SDI12_H
User-facing configuration for ProtoCore.