ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
mbus.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 mbus.h
6 * @brief Wired M-Bus (Meter-Bus, EN 13757-2/-3) frame codec (PC_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"
33
34#if PC_ENABLE_MBUS
35
36#include <stddef.h>
37#include <stdint.h>
38
39#define MBUS_START_SHORT 0x10u ///< short-frame start octet
40#define MBUS_START_LONG 0x68u ///< long / control-frame start octet
41#define MBUS_STOP 0x16u ///< frame stop octet
42#define MBUS_ACK 0xE5u ///< single-character acknowledge
43
44// Common control-field (C) values.
45#define MBUS_C_SND_NKE 0x40u ///< initialize slave (link reset)
46#define MBUS_C_REQ_UD2 0x5Bu ///< request class-2 user data (FCB=0); 0x7B with FCB=1
47#define MBUS_C_REQ_UD1 0x5Au ///< request class-1 user data
48#define MBUS_C_SND_UD 0x53u ///< send user data to slave (FCB=0); 0x73 with FCB=1
49#define MBUS_C_RSP_UD 0x08u ///< response with user data (+ ACD/DFC bits)
50
51// Common control-information (CI) values.
52#define MBUS_CI_DATA_SEND 0x51u ///< data send (master -> slave)
53#define MBUS_CI_SELECT 0x52u ///< selection of slaves
54#define MBUS_CI_RSP_VARIABLE 0x72u ///< variable data response, long header (LSB first)
55#define MBUS_CI_RSP_FIXED 0x73u ///< fixed data response
56
57#define MBUS_MAX_DATA 252u ///< max user-data octets (L is one octet; 255 - 3)
58
59/** @brief M-Bus frame kinds. */
60enum class MbusFrameType : uint8_t
61{
62 MBUS_FRAME_NONE = 0,
63 MBUS_FRAME_ACK, ///< single 0xE5
64 MBUS_FRAME_SHORT, ///< 10 C A CS 16
65 MBUS_FRAME_LONG, ///< 68 L L 68 C A CI ... CS 16 (control frame = long with no data)
66};
67
68/** @brief A parsed M-Bus frame (data points into the caller's buffer). */
69struct MbusFrame
70{
71 MbusFrameType type;
72 uint8_t c; ///< control field (short / long)
73 uint8_t a; ///< address field (short / long)
74 uint8_t ci; ///< control-information field (long only)
75 const uint8_t *data; ///< user data (long only), or nullptr
76 uint8_t data_len; ///< user-data length (long only)
77};
78
79// DIF data-field coding (low nibble of the DIF). The decoded fixed lengths are in octets.
80enum class MbusDifCoding : uint8_t
81{
82 MBUS_DIF_NONE = 0x0, ///< no data
83 MBUS_DIF_INT8 = 0x1, ///< 8-bit integer
84 MBUS_DIF_INT16 = 0x2, ///< 16-bit integer
85 MBUS_DIF_INT24 = 0x3, ///< 24-bit integer
86 MBUS_DIF_INT32 = 0x4, ///< 32-bit integer
87 MBUS_DIF_REAL32 = 0x5, ///< 32-bit IEEE-754 real
88 MBUS_DIF_INT48 = 0x6, ///< 48-bit integer
89 MBUS_DIF_INT64 = 0x7, ///< 64-bit integer
90 MBUS_DIF_READOUT = 0x8, ///< selection for readout (no data)
91 MBUS_DIF_BCD2 = 0x9, ///< 2-digit BCD (1 octet)
92 MBUS_DIF_BCD4 = 0xA, ///< 4-digit BCD (2 octets)
93 MBUS_DIF_BCD6 = 0xB, ///< 6-digit BCD (3 octets)
94 MBUS_DIF_BCD8 = 0xC, ///< 8-digit BCD (4 octets)
95 MBUS_DIF_VARIABLE = 0xD, ///< variable length (LVAR octet precedes the data)
96 MBUS_DIF_BCD12 = 0xE, ///< 12-digit BCD (6 octets)
97 MBUS_DIF_SPECIAL = 0xF, ///< special functions (no data)
98};
99
100/** @brief One decoded EN 13757-3 data record. */
101struct MbusRecord
102{
103 uint8_t dif; ///< first data-information octet
104 uint8_t coding; ///< DIF low nibble (see MbusDifCoding)
105 uint8_t vif; ///< first value-information octet (0 if none)
106 const uint8_t *data; ///< value octets (points into the caller buffer)
107 uint8_t data_len; ///< value length in octets
108};
109
110// --- builders: write into @p buf (cap), return frame length or 0 on overflow ---
111
112/** @brief Single-character acknowledge (0xE5). */
113size_t pc_mbus_build_ack(uint8_t *buf, size_t cap);
114
115/** @brief Short frame: 10 C A CS 16. */
116size_t pc_mbus_build_short(uint8_t *buf, size_t cap, uint8_t c, uint8_t a);
117
118/** @brief Long frame: 68 L L 68 C A CI [data] CS 16. @p data_len 0 builds a control frame. */
119size_t pc_mbus_build_long(uint8_t *buf, size_t cap, uint8_t c, uint8_t a, uint8_t ci, const uint8_t *data,
120 uint8_t data_len);
121
122/** @brief Convenience: a SND_NKE (link reset) short frame to address @p a. */
123size_t pc_mbus_build_snd_nke(uint8_t *buf, size_t cap, uint8_t a);
124
125/** @brief Convenience: a REQ_UD2 short frame to address @p a (@p fcb toggles the FCB bit). */
126size_t pc_mbus_build_req_ud2(uint8_t *buf, size_t cap, uint8_t a, bool fcb);
127
128/** @brief Convenience: a REQ_UD1 (class-1 / alarm data request) short frame to address @p a (@p fcb toggles
129 * the FCB bit). Where REQ_UD2 fetches routine class-2 data, REQ_UD1 fetches class-1 (alarm) data. */
130size_t pc_mbus_build_req_ud1(uint8_t *buf, size_t cap, uint8_t a, bool fcb);
131
132// --- parser ---
133
134/**
135 * @brief Parse one M-Bus frame from @p buf. Validates the start/stop octets, the doubled
136 * length, and the checksum. On success fills @p out and sets @p consumed to the frame length.
137 */
138bool pc_mbus_parse(const uint8_t *buf, size_t len, MbusFrame *out, size_t *consumed);
139
140// --- variable-data records (DIF / VIF) ---
141
142/** @brief Map a DIF low-nibble coding to its fixed data length (0 for variable / none). */
143uint8_t pc_mbus_dif_data_len(uint8_t coding);
144
145/**
146 * @brief Walk one data record at @p *pos within a long-frame body (the octets after CI).
147 * Skips DIFE / VIFE extension chains, decodes the data length (incl. the LVAR variable form),
148 * and advances @p *pos past the record. Returns false at the end of data or on overflow.
149 */
150bool pc_mbus_record_next(const uint8_t *body, size_t len, size_t *pos, MbusRecord *out);
151
152// --- record value + unit decoding ---
153
154/**
155 * @brief Decode a record's value as a signed 64-bit integer (the integer and BCD DIF codings).
156 *
157 * Integer codings are little-endian and sign-extended; BCD codings are little-endian octets of two
158 * digits, with a 0xF most-significant nibble marking a negative value. @return false for a real / variable
159 * / no-data coding, or an invalid BCD nibble.
160 */
161bool pc_mbus_record_value_int(const MbusRecord *r, int64_t *out);
162
163/** @brief Decode a record's value as an IEEE-754 float (only the REAL32 DIF coding). @return false otherwise. */
164bool pc_mbus_record_value_real(const MbusRecord *r, float *out);
165
166/** @brief Physical unit a VIF decodes to (the common EN 13757-3 measurement ranges). */
167enum class MbusUnit : uint8_t
168{
169 MBUS_UNIT_UNKNOWN = 0,
170 MBUS_UNIT_WH, ///< energy, watt-hours
171 MBUS_UNIT_J, ///< energy, joules
172 MBUS_UNIT_M3, ///< volume, cubic metres
173 MBUS_UNIT_KG, ///< mass, kilograms
174 MBUS_UNIT_W, ///< power, watts
175 MBUS_UNIT_J_PER_H, ///< power, joules per hour
176 MBUS_UNIT_M3_PER_H, ///< volume flow, cubic metres per hour
177 MBUS_UNIT_CELSIUS, ///< temperature, degrees Celsius
178 MBUS_UNIT_K, ///< temperature difference, kelvin
179 MBUS_UNIT_BAR, ///< pressure, bar
180};
181
182/**
183 * @brief Decode a VIF octet into its unit and the base-10 exponent applied to the raw value.
184 *
185 * Covers the common EN 13757-3 main-table measurement ranges (energy, volume, mass, power, volume flow,
186 * temperature, pressure). The physical value is (raw value) * 10^(@p exp10) in @p unit.
187 * @return true for a decoded measurement VIF; false (unit UNKNOWN) for one outside those ranges.
188 */
189bool pc_mbus_vif_decode(uint8_t vif, MbusUnit *unit, int8_t *exp10);
190
191// --- variable-data-structure fixed header (EN 13757-3): the 12 octets before the data records ---
192//
193// A CI = MBUS_CI_RSP_VARIABLE (0x72) long-frame body opens with a fixed header identifying the meter -
194// its secondary-address serial, manufacturer, version, and medium - then the access / status / signature,
195// and only then the DIF/VIF data records that pc_mbus_record_next walks.
196
197#define MBUS_VAR_HEADER_LEN 12u ///< octets of the fixed header preceding the records in a CI=0x72 response
198
199// Common medium / device-type codes (EN 13757-3 ยง6.4).
200#define MBUS_MEDIUM_OTHER 0x00u
201#define MBUS_MEDIUM_OIL 0x01u
202#define MBUS_MEDIUM_ELECTRICITY 0x02u
203#define MBUS_MEDIUM_GAS 0x03u
204#define MBUS_MEDIUM_HEAT_OUTLET 0x04u
205#define MBUS_MEDIUM_STEAM 0x05u
206#define MBUS_MEDIUM_WARM_WATER 0x06u
207#define MBUS_MEDIUM_WATER 0x07u
208#define MBUS_MEDIUM_HEAT_COST 0x08u
209#define MBUS_MEDIUM_HEAT_INLET 0x0Cu
210#define MBUS_MEDIUM_HEAT_COOLING 0x0Du
211#define MBUS_MEDIUM_COLD_WATER 0x16u
212
213/** @brief The decoded EN 13757-3 variable-data-structure fixed header. */
214struct MbusVarHeader
215{
216 uint32_t id; ///< identification number (secondary-address serial), decoded from the 4-octet BCD
217 char manufacturer[4]; ///< 3-letter manufacturer code + NUL (decoded from the 2-octet field)
218 uint16_t manufacturer_raw; ///< the raw 2-octet manufacturer value (FLAG code)
219 uint8_t version; ///< device generation / version
220 uint8_t medium; ///< medium / device type (MBUS_MEDIUM_*)
221 uint8_t access_no; ///< access number (increments each readout)
222 uint8_t status; ///< status octet (error / alarm bits)
223 uint16_t signature; ///< signature word (usually 0)
224};
225
226/**
227 * @brief Decode the 12-octet variable-data-structure fixed header (the header that precedes the data records
228 * in a CI = MBUS_CI_RSP_VARIABLE (0x72) long-frame body) into @p out.
229 * @return true iff @p len is at least 12 octets and the identification number is valid BCD; false otherwise.
230 */
231bool pc_mbus_parse_var_header(const uint8_t *body, size_t len, MbusVarHeader *out);
232
233#endif // PC_ENABLE_MBUS
234#endif // PROTOCORE_MBUS_H
User-facing configuration for ProtoCore.