DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 (DETWS_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; the UART transport is the application's. Bridge meters onto Wi-Fi by polling
22 * REQ_UD2 and publishing the decoded records.
23 *
24 * @author Douglas Quigg (dstroy0)
25 * @date 2026
26 */
27
28#ifndef DETERMINISTICESPASYNCWEBSERVER_MBUS_H
29#define DETERMINISTICESPASYNCWEBSERVER_MBUS_H
30
31#include "ServerConfig.h"
32
33#if DETWS_ENABLE_MBUS
34
35#include <stddef.h>
36#include <stdint.h>
37
38#define MBUS_START_SHORT 0x10u ///< short-frame start octet
39#define MBUS_START_LONG 0x68u ///< long / control-frame start octet
40#define MBUS_STOP 0x16u ///< frame stop octet
41#define MBUS_ACK 0xE5u ///< single-character acknowledge
42
43// Common control-field (C) values.
44#define MBUS_C_SND_NKE 0x40u ///< initialize slave (link reset)
45#define MBUS_C_REQ_UD2 0x5Bu ///< request class-2 user data (FCB=0); 0x7B with FCB=1
46#define MBUS_C_REQ_UD1 0x5Au ///< request class-1 user data
47#define MBUS_C_SND_UD 0x53u ///< send user data to slave (FCB=0); 0x73 with FCB=1
48#define MBUS_C_RSP_UD 0x08u ///< response with user data (+ ACD/DFC bits)
49
50// Common control-information (CI) values.
51#define MBUS_CI_DATA_SEND 0x51u ///< data send (master -> slave)
52#define MBUS_CI_SELECT 0x52u ///< selection of slaves
53#define MBUS_CI_RSP_VARIABLE 0x72u ///< variable data response, long header (LSB first)
54#define MBUS_CI_RSP_FIXED 0x73u ///< fixed data response
55
56#define MBUS_MAX_DATA 252u ///< max user-data octets (L is one octet; 255 - 3)
57
58/** @brief M-Bus frame kinds. */
59enum class MbusFrameType : uint8_t
60{
61 MBUS_FRAME_NONE = 0,
62 MBUS_FRAME_ACK, ///< single 0xE5
63 MBUS_FRAME_SHORT, ///< 10 C A CS 16
64 MBUS_FRAME_LONG, ///< 68 L L 68 C A CI ... CS 16 (control frame = long with no data)
65};
66
67/** @brief A parsed M-Bus frame (data points into the caller's buffer). */
68struct MbusFrame
69{
70 MbusFrameType type;
71 uint8_t c; ///< control field (short / long)
72 uint8_t a; ///< address field (short / long)
73 uint8_t ci; ///< control-information field (long only)
74 const uint8_t *data; ///< user data (long only), or nullptr
75 uint8_t data_len; ///< user-data length (long only)
76};
77
78// DIF data-field coding (low nibble of the DIF). The decoded fixed lengths are in octets.
79enum class MbusDifCoding : uint8_t
80{
81 MBUS_DIF_NONE = 0x0, ///< no data
82 MBUS_DIF_INT8 = 0x1, ///< 8-bit integer
83 MBUS_DIF_INT16 = 0x2, ///< 16-bit integer
84 MBUS_DIF_INT24 = 0x3, ///< 24-bit integer
85 MBUS_DIF_INT32 = 0x4, ///< 32-bit integer
86 MBUS_DIF_REAL32 = 0x5, ///< 32-bit IEEE-754 real
87 MBUS_DIF_INT48 = 0x6, ///< 48-bit integer
88 MBUS_DIF_INT64 = 0x7, ///< 64-bit integer
89 MBUS_DIF_READOUT = 0x8, ///< selection for readout (no data)
90 MBUS_DIF_BCD2 = 0x9, ///< 2-digit BCD (1 octet)
91 MBUS_DIF_BCD4 = 0xA, ///< 4-digit BCD (2 octets)
92 MBUS_DIF_BCD6 = 0xB, ///< 6-digit BCD (3 octets)
93 MBUS_DIF_BCD8 = 0xC, ///< 8-digit BCD (4 octets)
94 MBUS_DIF_VARIABLE = 0xD, ///< variable length (LVAR octet precedes the data)
95 MBUS_DIF_BCD12 = 0xE, ///< 12-digit BCD (6 octets)
96 MBUS_DIF_SPECIAL = 0xF, ///< special functions (no data)
97};
98
99/** @brief One decoded EN 13757-3 data record. */
100struct MbusRecord
101{
102 uint8_t dif; ///< first data-information octet
103 uint8_t coding; ///< DIF low nibble (see MbusDifCoding)
104 uint8_t vif; ///< first value-information octet (0 if none)
105 const uint8_t *data; ///< value octets (points into the caller buffer)
106 uint8_t data_len; ///< value length in octets
107};
108
109// --- builders: write into @p buf (cap), return frame length or 0 on overflow ---
110
111/** @brief Single-character acknowledge (0xE5). */
112size_t mbus_build_ack(uint8_t *buf, size_t cap);
113
114/** @brief Short frame: 10 C A CS 16. */
115size_t mbus_build_short(uint8_t *buf, size_t cap, uint8_t c, uint8_t a);
116
117/** @brief Long frame: 68 L L 68 C A CI [data] CS 16. @p data_len 0 builds a control frame. */
118size_t mbus_build_long(uint8_t *buf, size_t cap, uint8_t c, uint8_t a, uint8_t ci, const uint8_t *data,
119 uint8_t data_len);
120
121/** @brief Convenience: a SND_NKE (link reset) short frame to address @p a. */
122size_t mbus_build_snd_nke(uint8_t *buf, size_t cap, uint8_t a);
123
124/** @brief Convenience: a REQ_UD2 short frame to address @p a (@p fcb toggles the FCB bit). */
125size_t mbus_build_req_ud2(uint8_t *buf, size_t cap, uint8_t a, bool fcb);
126
127// --- parser ---
128
129/**
130 * @brief Parse one M-Bus frame from @p buf. Validates the start/stop octets, the doubled
131 * length, and the checksum. On success fills @p out and sets @p consumed to the frame length.
132 */
133bool mbus_parse(const uint8_t *buf, size_t len, MbusFrame *out, size_t *consumed);
134
135// --- variable-data records (DIF / VIF) ---
136
137/** @brief Map a DIF low-nibble coding to its fixed data length (0 for variable / none). */
138uint8_t mbus_dif_data_len(uint8_t coding);
139
140/**
141 * @brief Walk one data record at @p *pos within a long-frame body (the octets after CI).
142 * Skips DIFE / VIFE extension chains, decodes the data length (incl. the LVAR variable form),
143 * and advances @p *pos past the record. Returns false at the end of data or on overflow.
144 */
145bool mbus_record_next(const uint8_t *body, size_t len, size_t *pos, MbusRecord *out);
146
147#endif // DETWS_ENABLE_MBUS
148#endif // DETERMINISTICESPASYNCWEBSERVER_MBUS_H
User-facing configuration for DeterministicESPAsyncWebServer.