DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
mbus.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 mbus.cpp
6 * @brief Wired M-Bus (EN 13757) frame + data-record codec (pure, host-tested).
7 */
8
10
11#if DETWS_ENABLE_MBUS
12
13#include <string.h>
14
15// The M-Bus checksum is the 8-bit arithmetic sum of the covered octets.
16static uint8_t checksum(const uint8_t *p, size_t n)
17{
18 uint8_t s = 0;
19 for (size_t i = 0; i < n; i++)
20 s = (uint8_t)(s + p[i]);
21 return s;
22}
23
24size_t mbus_build_ack(uint8_t *buf, size_t cap)
25{
26 if (!buf || cap < 1)
27 return 0;
28 buf[0] = MBUS_ACK;
29 return 1;
30}
31
32size_t mbus_build_short(uint8_t *buf, size_t cap, uint8_t c, uint8_t a)
33{
34 if (!buf || cap < 5)
35 return 0;
36 buf[0] = MBUS_START_SHORT;
37 buf[1] = c;
38 buf[2] = a;
39 buf[3] = (uint8_t)(c + a); // checksum over C + A
40 buf[4] = MBUS_STOP;
41 return 5;
42}
43
44size_t mbus_build_long(uint8_t *buf, size_t cap, uint8_t c, uint8_t a, uint8_t ci, const uint8_t *data,
45 uint8_t data_len)
46{
47 if (!buf || data_len > MBUS_MAX_DATA || (data_len && !data))
48 return 0;
49 uint8_t L = (uint8_t)(3 + data_len); // L counts C + A + CI + user data
50 size_t total = (size_t)6 + L; // 68 L L 68 [L octets] CS 16
51 if (cap < total)
52 return 0;
53 buf[0] = MBUS_START_LONG;
54 buf[1] = L;
55 buf[2] = L;
56 buf[3] = MBUS_START_LONG;
57 buf[4] = c;
58 buf[5] = a;
59 buf[6] = ci;
60 if (data_len)
61 memcpy(buf + 7, data, data_len);
62 buf[4 + L] = checksum(buf + 4, L); // sum of C..end of user data
63 buf[5 + L] = MBUS_STOP;
64 return total;
65}
66
67size_t mbus_build_snd_nke(uint8_t *buf, size_t cap, uint8_t a)
68{
69 return mbus_build_short(buf, cap, MBUS_C_SND_NKE, a);
70}
71
72size_t mbus_build_req_ud2(uint8_t *buf, size_t cap, uint8_t a, bool fcb)
73{
74 return mbus_build_short(buf, cap, (uint8_t)(fcb ? 0x7Bu : MBUS_C_REQ_UD2), a);
75}
76
77bool mbus_parse(const uint8_t *buf, size_t len, MbusFrame *out, size_t *consumed)
78{
79 if (!buf || !out || len < 1)
80 return false;
81 out->type = MbusFrameType::MBUS_FRAME_NONE;
82 out->c = out->a = out->ci = 0;
83 out->data = nullptr;
84 out->data_len = 0;
85
86 if (buf[0] == MBUS_ACK)
87 {
88 out->type = MbusFrameType::MBUS_FRAME_ACK;
89 if (consumed)
90 *consumed = 1;
91 return true;
92 }
93 if (buf[0] == MBUS_START_SHORT)
94 {
95 if (len < 5 || buf[4] != MBUS_STOP)
96 return false;
97 if (buf[3] != (uint8_t)(buf[1] + buf[2]))
98 return false;
99 out->type = MbusFrameType::MBUS_FRAME_SHORT;
100 out->c = buf[1];
101 out->a = buf[2];
102 if (consumed)
103 *consumed = 5;
104 return true;
105 }
106 if (buf[0] == MBUS_START_LONG)
107 {
108 if (len < 4)
109 return false;
110 uint8_t L = buf[1];
111 if (L < 3 || buf[2] != L || buf[3] != MBUS_START_LONG)
112 return false;
113 size_t total = (size_t)6 + L;
114 if (len < total || buf[5 + L] != MBUS_STOP)
115 return false;
116 if (checksum(buf + 4, L) != buf[4 + L])
117 return false;
118 out->type = MbusFrameType::MBUS_FRAME_LONG;
119 out->c = buf[4];
120 out->a = buf[5];
121 out->ci = buf[6];
122 out->data_len = (uint8_t)(L - 3);
123 out->data = out->data_len ? buf + 7 : nullptr;
124 if (consumed)
125 *consumed = total;
126 return true;
127 }
128 return false;
129}
130
131uint8_t mbus_dif_data_len(uint8_t coding)
132{
133 switch ((MbusDifCoding)(coding & 0x0Fu))
134 {
135 case MbusDifCoding::MBUS_DIF_NONE:
136 return 0;
137 case MbusDifCoding::MBUS_DIF_INT8:
138 return 1;
139 case MbusDifCoding::MBUS_DIF_INT16:
140 return 2;
141 case MbusDifCoding::MBUS_DIF_INT24:
142 return 3;
143 case MbusDifCoding::MBUS_DIF_INT32:
144 return 4;
145 case MbusDifCoding::MBUS_DIF_REAL32:
146 return 4;
147 case MbusDifCoding::MBUS_DIF_INT48:
148 return 6;
149 case MbusDifCoding::MBUS_DIF_INT64:
150 return 8;
151 case MbusDifCoding::MBUS_DIF_READOUT:
152 return 0;
153 case MbusDifCoding::MBUS_DIF_BCD2:
154 return 1;
155 case MbusDifCoding::MBUS_DIF_BCD4:
156 return 2;
157 case MbusDifCoding::MBUS_DIF_BCD6:
158 return 3;
159 case MbusDifCoding::MBUS_DIF_BCD8:
160 return 4;
161 case MbusDifCoding::MBUS_DIF_VARIABLE:
162 return 0; // length carried in the LVAR octet
163 case MbusDifCoding::MBUS_DIF_BCD12:
164 return 6;
165 default: // MbusDifCoding::MBUS_DIF_SPECIAL
166 return 0;
167 }
168}
169
170bool mbus_record_next(const uint8_t *body, size_t len, size_t *pos, MbusRecord *out)
171{
172 if (!body || !pos || !out || *pos >= len)
173 return false;
174 size_t p = *pos;
175 uint8_t dif = body[p++];
176 uint8_t coding = (uint8_t)(dif & 0x0Fu);
177
178 // Skip the DIFE extension chain (each DIFE's high bit flags another).
179 uint8_t ext = dif;
180 while (ext & 0x80u)
181 {
182 if (p >= len)
183 return false;
184 ext = body[p++];
185 }
186
187 out->dif = dif;
188 out->coding = coding;
189 out->vif = 0;
190 out->data = nullptr;
191 out->data_len = 0;
192
193 if (coding == (uint8_t)MbusDifCoding::MBUS_DIF_SPECIAL) // manufacturer-specific / idle: no VIF, no fixed data
194 {
195 *pos = p;
196 return true;
197 }
198
199 // VIF (mandatory) + its VIFE extension chain.
200 if (p >= len)
201 return false;
202 out->vif = body[p++];
203 ext = out->vif;
204 while (ext & 0x80u)
205 {
206 if (p >= len)
207 return false;
208 ext = body[p++];
209 }
210
211 uint8_t dlen;
212 if (coding == (uint8_t)MbusDifCoding::MBUS_DIF_VARIABLE)
213 {
214 if (p >= len)
215 return false;
216 uint8_t lvar = body[p++];
217 if (lvar > 0xBFu)
218 return false; // only the LVAR raw/ASCII form (0x00..0xBF) is supported
219 dlen = lvar;
220 }
221 else
222 {
223 dlen = mbus_dif_data_len(coding);
224 }
225
226 if (p + dlen > len)
227 return false;
228 out->data = dlen ? body + p : nullptr;
229 out->data_len = dlen;
230 *pos = p + dlen;
231 return true;
232}
233
234#endif // DETWS_ENABLE_MBUS
Wired M-Bus (Meter-Bus, EN 13757-2/-3) frame codec (DETWS_ENABLE_MBUS).