ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
mbus.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 mbus.h
6 * @brief Wired M-Bus (Meter-Bus, EN 13757-2/-3) frame codec (PROTOCORE_ENABLE_MBUS).
7 *
8 * A pure, zero-heap builder + parser for the M-Bus link-layer frames used by utility meters
9 * (water / gas / heat / electricity), plus a walker for the EN 13757-3 variable-data records
10 * (DIF / VIF). M-Bus has three frame formats:
11 * @code
12 * single char : E5 (ACK)
13 * short frame : 10 C A CS 16
14 * long frame : 68 L L 68 C A CI [user data] CS 16 (L = 3 + data; CS = sum(C..data) mod 256)
15 * @endcode
16 * The control frame is just a long frame with no user data (L = 3). The checksum is the
17 * 8-bit sum of every octet from C through the end of the user data.
18 *
19 * The wired bus is a powered two-wire pair: the ESP32 talks to it over a UART through an
20 * M-Bus level converter (e.g. a TSS721-based master module). This codec is the framing +
21 * record layer, including decoding a record's raw value (integer / BCD / real) into a number and its VIF
22 * into a physical unit + decimal exponent; the UART transport is the application's. Bridge meters onto
23 * Wi-Fi by polling REQ_UD2 and publishing the decoded records.
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef PROTOCORE_MBUS_H
30#define PROTOCORE_MBUS_H
31
32#include "protocore_config.h" // the entry point: protocore_types.h for the widths
33
34#if PROTOCORE_ENABLE_MBUS
35
37
38// This module holds nothing between calls, so it carves no borrow and states none. An entry
39// takes one all the same, and never reads it, so every namespace in the tree is invoked the
40// same way.
41
42#define MBUS_START_SHORT 0x10u ///< short-frame start octet
43#define MBUS_START_LONG 0x68u ///< long / control-frame start octet
44#define MBUS_STOP 0x16u ///< frame stop octet
45#define MBUS_ACK 0xE5u ///< single-character acknowledge
46
47// Common control-field (C) values.
48#define MBUS_C_SND_NKE 0x40u ///< initialize slave (link reset)
49#define MBUS_C_REQ_UD2 0x5Bu ///< request class-2 user data (FCB=0); 0x7B with FCB=1
50#define MBUS_C_REQ_UD1 0x5Au ///< request class-1 user data
51#define MBUS_C_SND_UD 0x53u ///< send user data to slave (FCB=0); 0x73 with FCB=1
52#define MBUS_C_RSP_UD 0x08u ///< response with user data (+ ACD/DFC bits)
53
54// Common control-information (CI) values.
55#define MBUS_CI_DATA_SEND 0x51u ///< data send (master -> slave)
56#define MBUS_CI_SELECT 0x52u ///< selection of slaves
57#define MBUS_CI_RSP_VARIABLE 0x72u ///< variable data response, long header (LSB first)
58#define MBUS_CI_RSP_FIXED 0x73u ///< fixed data response
59
60#define MBUS_MAX_DATA 252u ///< max user-data octets (L is one octet; 255 - 3)
61
62#define MBUS_VAR_HEADER_LEN 12u ///< octets of the fixed header preceding the records in a CI=0x72 response
63
64// Common medium / device-type codes (EN 13757-3 ยง6.4).
65#define MBUS_MEDIUM_OTHER 0x00u
66#define MBUS_MEDIUM_OIL 0x01u
67#define MBUS_MEDIUM_ELECTRICITY 0x02u
68#define MBUS_MEDIUM_GAS 0x03u
69#define MBUS_MEDIUM_HEAT_OUTLET 0x04u
70#define MBUS_MEDIUM_STEAM 0x05u
71#define MBUS_MEDIUM_WARM_WATER 0x06u
72#define MBUS_MEDIUM_WATER 0x07u
73#define MBUS_MEDIUM_HEAT_COST 0x08u
74#define MBUS_MEDIUM_HEAT_INLET 0x0Cu
75#define MBUS_MEDIUM_HEAT_COOLING 0x0Du
76#define MBUS_MEDIUM_COLD_WATER 0x16u
77
78/** @brief M-Bus frame kinds. */
79typedef enum PROTO_ENUM_PACKED
80{
81 MBUS_FRAME_NONE = 0,
82 MBUS_FRAME_ACK, ///< single 0xE5
83 MBUS_FRAME_SHORT, ///< 10 C A CS 16
84 MBUS_FRAME_LONG, ///< 68 L L 68 C A CI ... CS 16 (control frame = long with no data)
85} MbusFrameType;
86
87/** @brief A parsed M-Bus frame (data points into the caller's buffer). */
88typedef struct
89{
90 MbusFrameType type;
91 uint8_t c; ///< control field (short / long)
92 uint8_t a; ///< address field (short / long)
93 uint8_t ci; ///< control-information field (long only)
94 const uint8_t *data; ///< user data (long only), or nullptr
95 uint8_t data_len; ///< user-data length (long only)
96} MbusFrame;
97
98typedef enum PROTO_ENUM_PACKED
99{
100 MBUS_DIF_NONE = 0x0, ///< no data
101 MBUS_DIF_INT8 = 0x1, ///< 8-bit integer
102 MBUS_DIF_INT16 = 0x2, ///< 16-bit integer
103 MBUS_DIF_INT24 = 0x3, ///< 24-bit integer
104 MBUS_DIF_INT32 = 0x4, ///< 32-bit integer
105 MBUS_DIF_REAL32 = 0x5, ///< 32-bit IEEE-754 real
106 MBUS_DIF_INT48 = 0x6, ///< 48-bit integer
107 MBUS_DIF_INT64 = 0x7, ///< 64-bit integer
108 MBUS_DIF_READOUT = 0x8, ///< selection for readout (no data)
109 MBUS_DIF_BCD2 = 0x9, ///< 2-digit BCD (1 octet)
110 MBUS_DIF_BCD4 = 0xA, ///< 4-digit BCD (2 octets)
111 MBUS_DIF_BCD6 = 0xB, ///< 6-digit BCD (3 octets)
112 MBUS_DIF_BCD8 = 0xC, ///< 8-digit BCD (4 octets)
113 MBUS_DIF_VARIABLE = 0xD, ///< variable length (LVAR octet precedes the data)
114 MBUS_DIF_BCD12 = 0xE, ///< 12-digit BCD (6 octets)
115 MBUS_DIF_SPECIAL = 0xF, ///< special functions (no data)
116} MbusDifCoding;
117
118/** @brief One decoded EN 13757-3 data record. */
119typedef struct
120{
121 uint8_t dif; ///< first data-information octet
122 uint8_t coding; ///< DIF low nibble (see MbusDifCoding)
123 uint8_t vif; ///< first value-information octet (0 if none)
124 const uint8_t *data; ///< value octets (points into the caller buffer)
125 uint8_t data_len; ///< value length in octets
126} MbusRecord;
127
128/** @brief Physical unit a VIF decodes to (the common EN 13757-3 measurement ranges). */
129typedef enum PROTO_ENUM_PACKED
130{
131 MBUS_UNIT_UNKNOWN = 0,
132 MBUS_UNIT_WH, ///< energy, watt-hours
133 MBUS_UNIT_J, ///< energy, joules
134 MBUS_UNIT_M3, ///< volume, cubic metres
135 MBUS_UNIT_KG, ///< mass, kilograms
136 MBUS_UNIT_W, ///< power, watts
137 MBUS_UNIT_J_PER_H, ///< power, joules per hour
138 MBUS_UNIT_M3_PER_H, ///< volume flow, cubic metres per hour
139 MBUS_UNIT_CELSIUS, ///< temperature, degrees Celsius
140 MBUS_UNIT_K, ///< temperature difference, kelvin
141 MBUS_UNIT_BAR, ///< pressure, bar
142} MbusUnit;
143
144/** @brief The decoded EN 13757-3 variable-data-structure fixed header. */
145typedef struct
146{
147 uint32_t id; ///< identification number (secondary-address serial), decoded from the 4-octet BCD
148 char manufacturer[4]; ///< 3-letter manufacturer code + NUL (decoded from the 2-octet field)
149 uint16_t manufacturer_raw; ///< the raw 2-octet manufacturer value (FLAG code)
150 uint8_t version; ///< device generation / version
151 uint8_t medium; ///< medium / device type (MBUS_MEDIUM_*)
152 uint8_t access_no; ///< access number (increments each readout)
153 uint8_t status; ///< status octet (error / alarm bits)
154 uint16_t signature; ///< signature word (usually 0)
155} MbusVarHeader;
156
157/** @brief What build_ack takes: buf, cap. */
158typedef struct
159{
160 uint8_t *buf;
161 size_t cap;
162} MbusBuildAckArgs;
163
164/** @brief What build_short takes: buf, cap, c, a. */
165typedef struct
166{
167 uint8_t *buf;
168 size_t cap;
169 uint8_t c;
170 uint8_t a;
171} MbusBuildShortArgs;
172
173/** @brief What build_long takes: buf, cap, c, a, ci, data, data_len. */
174typedef struct
175{
176 uint8_t *buf;
177 size_t cap;
178 uint8_t c;
179 uint8_t a;
180 uint8_t ci;
181 const uint8_t *data;
182 uint8_t data_len;
183} MbusBuildLongArgs;
184
185/** @brief What build_snd_nke takes: buf, cap, a. */
186typedef struct
187{
188 uint8_t *buf;
189 size_t cap;
190 uint8_t a;
191} MbusBuildSndNkeArgs;
192
193/** @brief What build_req_ud2 takes: buf, cap, a, fcb. */
194typedef struct
195{
196 uint8_t *buf;
197 size_t cap;
198 uint8_t a;
199 proto_bool fcb;
200} MbusBuildReqUd2Args;
201
202/** @brief What build_req_ud1 takes: buf, cap, a, fcb. */
203typedef struct
204{
205 uint8_t *buf;
206 size_t cap;
207 uint8_t a;
208 proto_bool fcb;
209} MbusBuildReqUd1Args;
210
211/** @brief What parse takes: buf, len, out, consumed. */
212typedef struct
213{
214 const uint8_t *buf;
215 size_t len;
216 MbusFrame *out;
217 size_t *consumed;
218} MbusParseArgs;
219
220/** @brief What dif_data_len takes: coding. */
221typedef struct
222{
223 uint8_t coding;
224} MbusDifDataLenArgs;
225
226/** @brief What record_next takes: body, len, pos, out. */
227typedef struct
228{
229 const uint8_t *body;
230 size_t len;
231 size_t *pos;
232 MbusRecord *out;
233} MbusRecordNextArgs;
234
235/** @brief What record_value_int takes: r, out. */
236typedef struct
237{
238 const MbusRecord *r;
239 int64_t *out;
240} MbusRecordValueIntArgs;
241
242/** @brief What record_value_real takes: r, out. */
243typedef struct
244{
245 const MbusRecord *r;
246 float *out;
247} MbusRecordValueRealArgs;
248
249/** @brief What vif_decode takes: vif, unit, exp10. */
250typedef struct
251{
252 uint8_t vif;
253 MbusUnit *unit;
254 int8_t *exp10;
255} MbusVifDecodeArgs;
256
257/** @brief What parse_var_header takes: body, len, out. */
258typedef struct
259{
260 const uint8_t *body;
261 size_t len;
262 MbusVarHeader *out;
263} MbusParseVarHeaderArgs;
264
265/**
266 * @brief Wired M-Bus (Meter-Bus, EN 13757-2/-3) frame codec (PROTOCORE_ENABLE_MBUS).
267 *
268 * A caller sets the members a call takes, invokes it through ::Mbus with the bytes it runs
269 * out of, and reads the outcome off the same handle.
270 *
271 * Mbus.build_ack_args.buf = ...;
272 * Mbus.build_ack_args.cap = ...;
273 * Mbus.build_ack(work);
274 * // Mbus.n is what the call reports
275 *
276 * @var MbusNs::build_ack_args what build_ack takes: buf, cap
277 * @var MbusNs::build_short_args what build_short takes: buf, cap, c, a
278 * @var MbusNs::build_long_args what build_long takes: buf, cap, c, a, ci, data, data_len
279 * @var MbusNs::build_snd_nke_args what build_snd_nke takes: buf, cap, a
280 * @var MbusNs::build_req_ud2_args what build_req_ud2 takes: buf, cap, a, fcb
281 * @var MbusNs::build_req_ud1_args what build_req_ud1 takes: buf, cap, a, fcb
282 * @var MbusNs::parse_args what parse takes: buf, len, out, consumed
283 * @var MbusNs::dif_data_len_args what dif_data_len takes: coding
284 * @var MbusNs::record_next_args what record_next takes: body, len, pos, out
285 * @var MbusNs::record_value_int_args what record_value_int takes: r, out
286 * @var MbusNs::record_value_real_args what record_value_real takes: r, out
287 * @var MbusNs::vif_decode_args what vif_decode takes: vif, unit, exp10
288 * @var MbusNs::parse_var_header_args what parse_var_header takes: body, len, out
289 * @var MbusNs::ok true for a decoded measurement VIF; false (unit UNKNOWN) for one ...
290 * @var MbusNs::n the count a call reports
291 * @var MbusNs::value the value a call reports
292 * @var MbusNs::build_ack single-character acknowledge (0xE5)
293 * @var MbusNs::build_short short frame: 10 C A CS 16
294 * @var MbusNs::build_long long frame: 68 L L 68 C A CI [data] CS 16. data_len 0 builds a ...
295 * @var MbusNs::build_snd_nke convenience: a SND_NKE (link reset) short frame to address a
296 * @var MbusNs::build_req_ud2 convenience: a REQ_UD2 short frame to address a (fcb toggles the ...
297 * @var MbusNs::build_req_ud1 convenience: a REQ_UD1 (class-1 / alarm data request) short frame ...
298 * @var MbusNs::parse parse one M-Bus frame from buf. Validates the start/stop octets, ...
299 * @var MbusNs::dif_data_len map a DIF low-nibble coding to its fixed data length (0 for ...
300 * @var MbusNs::record_next walk one data record at *pos within a long-frame body (the octets ...
301 * @var MbusNs::record_value_int decode a record's value as a signed 64-bit integer (the integer and ...
302 * @var MbusNs::record_value_real decode a record's value as an IEEE-754 float (only the REAL32 DIF ...
303 * @var MbusNs::vif_decode decode a VIF octet into its unit and the base-10 exponent applied ...
304 * @var MbusNs::parse_var_header decode the 12-octet variable-data-structure fixed header (the ...
305 *
306 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
307 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
308 * a caller drives every namespace the same way.
309 */
310typedef struct
311{
312 MbusBuildAckArgs build_ack_args;
313 MbusBuildShortArgs build_short_args;
314 MbusBuildLongArgs build_long_args;
315 MbusBuildSndNkeArgs build_snd_nke_args;
316 MbusBuildReqUd2Args build_req_ud2_args;
317 MbusBuildReqUd1Args build_req_ud1_args;
318 MbusParseArgs parse_args;
319 MbusDifDataLenArgs dif_data_len_args;
320 MbusRecordNextArgs record_next_args;
321 MbusRecordValueIntArgs record_value_int_args;
322 MbusRecordValueRealArgs record_value_real_args;
323 MbusVifDecodeArgs vif_decode_args;
324 MbusParseVarHeaderArgs parse_var_header_args;
325 proto_bool ok;
326 size_t n;
327 uint8_t value;
328} MbusVars;
329
330/** @brief The operands and the outcome. */
331extern MbusVars MbusV;
332
333/** @brief The entries. */
334typedef struct
335{
336 void (*const build_ack)(uint8_t *work);
337 void (*const build_short)(uint8_t *work);
338 void (*const build_long)(uint8_t *work);
339 void (*const build_snd_nke)(uint8_t *work);
340 void (*const build_req_ud2)(uint8_t *work);
341 void (*const build_req_ud1)(uint8_t *work);
342 void (*const parse)(uint8_t *work);
343 void (*const dif_data_len)(uint8_t *work);
344 void (*const record_next)(uint8_t *work);
345 void (*const record_value_int)(uint8_t *work);
346 void (*const record_value_real)(uint8_t *work);
347 void (*const vif_decode)(uint8_t *work);
348 void (*const parse_var_header)(uint8_t *work);
349} MbusNs;
350
351// What the table binds, defined once in the .c and taking one parameter each: everything
352// else an entry needs is an operand in MbusV or a region of the borrow at a fixed offset.
353void protocore_mbus_build_ack(uint8_t *work);
354void protocore_mbus_build_short(uint8_t *work);
355void protocore_mbus_build_long(uint8_t *work);
356void protocore_mbus_build_snd_nke(uint8_t *work);
357void protocore_mbus_build_req_ud2(uint8_t *work);
358void protocore_mbus_build_req_ud1(uint8_t *work);
359void protocore_mbus_parse(uint8_t *work);
360void protocore_mbus_dif_data_len(uint8_t *work);
361void protocore_mbus_record_next(uint8_t *work);
362void protocore_mbus_record_value_int(uint8_t *work);
363void protocore_mbus_record_value_real(uint8_t *work);
364void protocore_mbus_vif_decode(uint8_t *work);
365void protocore_mbus_parse_var_header(uint8_t *work);
366
367// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
368// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
369// `Mbus.build_ack(work)` resolves to a named function and becomes a DIRECT call. An extern table
370// leaves the call indirect and the symbol live at every level, -O2 -flto included.
371static const MbusNs Mbus __attribute__((unused)) = {
372 .build_ack = protocore_mbus_build_ack,
373 .build_short = protocore_mbus_build_short,
374 .build_long = protocore_mbus_build_long,
375 .build_snd_nke = protocore_mbus_build_snd_nke,
376 .build_req_ud2 = protocore_mbus_build_req_ud2,
377 .build_req_ud1 = protocore_mbus_build_req_ud1,
378 .parse = protocore_mbus_parse,
379 .dif_data_len = protocore_mbus_dif_data_len,
380 .record_next = protocore_mbus_record_next,
381 .record_value_int = protocore_mbus_record_value_int,
382 .record_value_real = protocore_mbus_record_value_real,
383 .vif_decode = protocore_mbus_vif_decode,
384 .parse_var_header = protocore_mbus_parse_var_header,
385};
386
388
389#endif // PROTOCORE_ENABLE_MBUS
390
391#endif // PROTOCORE_MBUS_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#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