ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_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 {
21 s = (uint8_t)(s + p[i]);
22 }
23 return s;
24}
25
26size_t pc_mbus_build_ack(uint8_t *buf, size_t cap)
27{
28 if (!buf || cap < 1)
29 {
30 return 0;
31 }
32 buf[0] = MBUS_ACK;
33 return 1;
34}
35
36size_t pc_mbus_build_short(uint8_t *buf, size_t cap, uint8_t c, uint8_t a)
37{
38 if (!buf || cap < 5)
39 {
40 return 0;
41 }
42 buf[0] = MBUS_START_SHORT;
43 buf[1] = c;
44 buf[2] = a;
45 buf[3] = (uint8_t)(c + a); // checksum over C + A
46 buf[4] = MBUS_STOP;
47 return 5;
48}
49
50size_t pc_mbus_build_long(uint8_t *buf, size_t cap, uint8_t c, uint8_t a, uint8_t ci, const uint8_t *data,
51 uint8_t data_len)
52{
53 if (!buf || data_len > MBUS_MAX_DATA || (data_len && !data))
54 {
55 return 0;
56 }
57 uint8_t L = (uint8_t)(3 + data_len); // L counts C + A + CI + user data
58 size_t total = (size_t)6 + L; // 68 L L 68 [L octets] CS 16
59 if (cap < total)
60 {
61 return 0;
62 }
63 buf[0] = MBUS_START_LONG;
64 buf[1] = L;
65 buf[2] = L;
66 buf[3] = MBUS_START_LONG;
67 buf[4] = c;
68 buf[5] = a;
69 buf[6] = ci;
70 if (data_len)
71 {
72 memcpy(buf + 7, data, data_len);
73 }
74 buf[4 + L] = checksum(buf + 4, L); // sum of C..end of user data
75 buf[5 + L] = MBUS_STOP;
76 return total;
77}
78
79size_t pc_mbus_build_snd_nke(uint8_t *buf, size_t cap, uint8_t a)
80{
81 return pc_mbus_build_short(buf, cap, MBUS_C_SND_NKE, a);
82}
83
84size_t pc_mbus_build_req_ud2(uint8_t *buf, size_t cap, uint8_t a, bool fcb)
85{
86 return pc_mbus_build_short(buf, cap, (uint8_t)(fcb ? 0x7Bu : MBUS_C_REQ_UD2), a);
87}
88
89size_t pc_mbus_build_req_ud1(uint8_t *buf, size_t cap, uint8_t a, bool fcb)
90{
91 return pc_mbus_build_short(buf, cap, (uint8_t)(fcb ? 0x7Au : MBUS_C_REQ_UD1), a);
92}
93
94bool pc_mbus_parse(const uint8_t *buf, size_t len, MbusFrame *out, size_t *consumed)
95{
96 if (!buf || !out || len < 1)
97 {
98 return false;
99 }
100 out->type = MbusFrameType::MBUS_FRAME_NONE;
101 out->c = out->a = out->ci = 0;
102 out->data = nullptr;
103 out->data_len = 0;
104
105 if (buf[0] == MBUS_ACK)
106 {
107 out->type = MbusFrameType::MBUS_FRAME_ACK;
108 if (consumed)
109 {
110 *consumed = 1;
111 }
112 return true;
113 }
114 if (buf[0] == MBUS_START_SHORT)
115 {
116 if (len < 5 || buf[4] != MBUS_STOP)
117 {
118 return false;
119 }
120 if (buf[3] != (uint8_t)(buf[1] + buf[2]))
121 {
122 return false;
123 }
124 out->type = MbusFrameType::MBUS_FRAME_SHORT;
125 out->c = buf[1];
126 out->a = buf[2];
127 if (consumed)
128 {
129 *consumed = 5;
130 }
131 return true;
132 }
133 if (buf[0] == MBUS_START_LONG)
134 {
135 if (len < 4)
136 {
137 return false;
138 }
139 uint8_t L = buf[1];
140 if (L < 3 || buf[2] != L || buf[3] != MBUS_START_LONG)
141 {
142 return false;
143 }
144 size_t total = (size_t)6 + L;
145 if (len < total || buf[5 + L] != MBUS_STOP)
146 {
147 return false;
148 }
149 if (checksum(buf + 4, L) != buf[4 + L])
150 {
151 return false;
152 }
153 out->type = MbusFrameType::MBUS_FRAME_LONG;
154 out->c = buf[4];
155 out->a = buf[5];
156 out->ci = buf[6];
157 out->data_len = (uint8_t)(L - 3);
158 out->data = out->data_len ? buf + 7 : nullptr;
159 if (consumed)
160 {
161 *consumed = total;
162 }
163 return true;
164 }
165 return false;
166}
167
168uint8_t pc_mbus_dif_data_len(uint8_t coding)
169{
170 switch ((MbusDifCoding)(coding & 0x0Fu))
171 {
172 case MbusDifCoding::MBUS_DIF_NONE:
173 return 0;
174 case MbusDifCoding::MBUS_DIF_INT8:
175 return 1;
176 case MbusDifCoding::MBUS_DIF_INT16:
177 return 2;
178 case MbusDifCoding::MBUS_DIF_INT24:
179 return 3;
180 case MbusDifCoding::MBUS_DIF_INT32:
181 return 4;
182 case MbusDifCoding::MBUS_DIF_REAL32:
183 return 4;
184 case MbusDifCoding::MBUS_DIF_INT48:
185 return 6;
186 case MbusDifCoding::MBUS_DIF_INT64:
187 return 8;
188 case MbusDifCoding::MBUS_DIF_READOUT:
189 return 0;
190 case MbusDifCoding::MBUS_DIF_BCD2:
191 return 1;
192 case MbusDifCoding::MBUS_DIF_BCD4:
193 return 2;
194 case MbusDifCoding::MBUS_DIF_BCD6:
195 return 3;
196 case MbusDifCoding::MBUS_DIF_BCD8:
197 return 4;
198 case MbusDifCoding::MBUS_DIF_VARIABLE:
199 return 0; // length carried in the LVAR octet
200 case MbusDifCoding::MBUS_DIF_BCD12:
201 return 6;
202 default: // MbusDifCoding::MBUS_DIF_SPECIAL
203 return 0;
204 }
205}
206
207bool pc_mbus_record_next(const uint8_t *body, size_t len, size_t *pos, MbusRecord *out)
208{
209 if (!body || !pos || !out || *pos >= len)
210 {
211 return false;
212 }
213 size_t p = *pos;
214 uint8_t dif = body[p++];
215 uint8_t coding = (uint8_t)(dif & 0x0Fu);
216
217 // Skip the DIFE extension chain (each DIFE's high bit flags another).
218 uint8_t ext = dif;
219 while (ext & 0x80u)
220 {
221 if (p >= len)
222 {
223 return false;
224 }
225 ext = body[p++];
226 }
227
228 out->dif = dif;
229 out->coding = coding;
230 out->vif = 0;
231 out->data = nullptr;
232 out->data_len = 0;
233
234 if (coding == (uint8_t)MbusDifCoding::MBUS_DIF_SPECIAL) // manufacturer-specific / idle: no VIF, no fixed data
235 {
236 *pos = p;
237 return true;
238 }
239
240 // VIF (mandatory) + its VIFE extension chain.
241 if (p >= len)
242 {
243 return false;
244 }
245 out->vif = body[p++];
246 ext = out->vif;
247 while (ext & 0x80u)
248 {
249 if (p >= len)
250 {
251 return false;
252 }
253 ext = body[p++];
254 }
255
256 uint8_t dlen;
257 if (coding == (uint8_t)MbusDifCoding::MBUS_DIF_VARIABLE)
258 {
259 if (p >= len)
260 {
261 return false;
262 }
263 uint8_t lvar = body[p++];
264 if (lvar > 0xBFu)
265 {
266 return false; // only the LVAR raw/ASCII form (0x00..0xBF) is supported
267 }
268 dlen = lvar;
269 }
270 else
271 {
272 dlen = pc_mbus_dif_data_len(coding);
273 }
274
275 if (p + dlen > len)
276 {
277 return false;
278 }
279 out->data = dlen ? body + p : nullptr;
280 out->data_len = dlen;
281 *pos = p + dlen;
282 return true;
283}
284
285// --- record value + unit decoding ---
286
287// Decode a little-endian signed integer of @p n (1..8) octets (M-Bus INT8..INT64), sign-extended.
288static bool mbus_decode_int(const uint8_t *d, uint8_t n, int64_t *out)
289{
290 if (n == 0 || n > 8)
291 {
292 return false;
293 }
294 uint64_t u = 0;
295 for (int i = (int)n - 1; i >= 0; i--)
296 {
297 u = (u << 8) | d[i]; // little-endian
298 }
299 if (n < 8 && (d[n - 1] & 0x80u))
300 {
301 u |= ~(uint64_t)0 << (n * 8); // sign-extend the negative
302 }
303 *out = (int64_t)u;
304 return true;
305}
306
307// Decode a packed-BCD value of @p n (1..6) octets (M-Bus BCD2..BCD12); a 0xF top nibble marks negative.
308static bool mbus_decode_bcd(const uint8_t *d, uint8_t n, int64_t *out)
309{
310 if (n == 0 || n > 6)
311 {
312 return false;
313 }
314 bool neg = (uint8_t)(d[n - 1] >> 4) == 0x0Fu; // a 0xF top nibble marks a negative value
315 int64_t v = 0;
316 int64_t mult = 1;
317 for (uint8_t i = 0; i < n; i++)
318 {
319 uint8_t lo = (uint8_t)(d[i] & 0x0Fu);
320 uint8_t hi = (uint8_t)(d[i] >> 4);
321 if (i == (uint8_t)(n - 1) && neg)
322 {
323 hi = 0;
324 }
325 if (lo > 9 || hi > 9) // an invalid BCD nibble
326 {
327 return false;
328 }
329 v += (int64_t)(hi * 10 + lo) * mult;
330 mult *= 100;
331 }
332 *out = neg ? -v : v;
333 return true;
334}
335
336bool pc_mbus_record_value_int(const MbusRecord *r, int64_t *out)
337{
338 if (!r || !out || !r->data)
339 {
340 return false;
341 }
342 const uint8_t *d = r->data;
343 uint8_t n = r->data_len;
344 switch ((MbusDifCoding)r->coding)
345 {
346 case MbusDifCoding::MBUS_DIF_INT8:
347 case MbusDifCoding::MBUS_DIF_INT16:
348 case MbusDifCoding::MBUS_DIF_INT24:
349 case MbusDifCoding::MBUS_DIF_INT32:
350 case MbusDifCoding::MBUS_DIF_INT48:
351 case MbusDifCoding::MBUS_DIF_INT64:
352 return mbus_decode_int(d, n, out);
353 case MbusDifCoding::MBUS_DIF_BCD2:
354 case MbusDifCoding::MBUS_DIF_BCD4:
355 case MbusDifCoding::MBUS_DIF_BCD6:
356 case MbusDifCoding::MBUS_DIF_BCD8:
357 case MbusDifCoding::MBUS_DIF_BCD12:
358 return mbus_decode_bcd(d, n, out);
359 default:
360 return false; // real / variable / no-data codings are not integers
361 }
362}
363
364bool pc_mbus_record_value_real(const MbusRecord *r, float *out)
365{
366 if (!r || !out || !r->data || (MbusDifCoding)r->coding != MbusDifCoding::MBUS_DIF_REAL32 || r->data_len < 4)
367 {
368 return false;
369 }
370 uint32_t bits = (uint32_t)r->data[0] | ((uint32_t)r->data[1] << 8) | ((uint32_t)r->data[2] << 16) |
371 ((uint32_t)r->data[3] << 24);
372 memcpy(out, &bits, 4);
373 return true;
374}
375
376bool pc_mbus_vif_decode(uint8_t vif, MbusUnit *unit, int8_t *exp10)
377{
378 MbusUnit u = MbusUnit::MBUS_UNIT_UNKNOWN;
379 int8_t e = 0;
380 uint8_t v = (uint8_t)(vif & 0x7Fu); // ignore the VIF extension bit
381 if (v <= 0x07)
382 {
383 u = MbusUnit::MBUS_UNIT_WH;
384 e = (int8_t)((v & 7) - 3); // energy 10^(nnn-3) Wh
385 }
386 else if (v <= 0x0F)
387 {
388 u = MbusUnit::MBUS_UNIT_J;
389 e = (int8_t)(v & 7); // energy 10^(nnn) J
390 }
391 else if (v <= 0x17)
392 {
393 u = MbusUnit::MBUS_UNIT_M3;
394 e = (int8_t)((v & 7) - 6); // volume 10^(nnn-6) m3
395 }
396 else if (v <= 0x1F)
397 {
398 u = MbusUnit::MBUS_UNIT_KG;
399 e = (int8_t)((v & 7) - 3); // mass 10^(nnn-3) kg
400 }
401 else if (v >= 0x28 && v <= 0x2F)
402 {
403 u = MbusUnit::MBUS_UNIT_W;
404 e = (int8_t)((v & 7) - 3); // power 10^(nnn-3) W
405 }
406 else if (v >= 0x30 && v <= 0x37)
407 {
408 u = MbusUnit::MBUS_UNIT_J_PER_H;
409 e = (int8_t)(v & 7); // power 10^(nnn) J/h
410 }
411 else if (v >= 0x38 && v <= 0x3F)
412 {
413 u = MbusUnit::MBUS_UNIT_M3_PER_H;
414 e = (int8_t)((v & 7) - 6); // volume flow 10^(nnn-6) m3/h
415 }
416 else if ((v >= 0x58 && v <= 0x5F) || (v >= 0x64 && v <= 0x67))
417 {
418 // Flow/return (0x58..0x5F) and external (0x64..0x67) temperatures are both degrees Celsius at
419 // the same 10^(nn-3) scale, so they share one branch (the VIF only distinguishes them semantically).
420 u = MbusUnit::MBUS_UNIT_CELSIUS;
421 e = (int8_t)((v & 3) - 3);
422 }
423 else if (v >= 0x60 && v <= 0x63)
424 {
425 u = MbusUnit::MBUS_UNIT_K;
426 e = (int8_t)((v & 3) - 3); // temperature difference 10^(nn-3) K
427 }
428 else if (v >= 0x68 && v <= 0x6B)
429 {
430 u = MbusUnit::MBUS_UNIT_BAR;
431 e = (int8_t)((v & 3) - 3); // pressure 10^(nn-3) bar
432 }
433 if (unit)
434 {
435 *unit = u;
436 }
437 if (exp10)
438 {
439 *exp10 = e;
440 }
441 return u != MbusUnit::MBUS_UNIT_UNKNOWN;
442}
443
444bool pc_mbus_parse_var_header(const uint8_t *body, size_t len, MbusVarHeader *out)
445{
446 if (!body || !out || len < MBUS_VAR_HEADER_LEN)
447 {
448 return false;
449 }
450 // Identification number: 4 octets of BCD, least-significant octet first.
451 uint32_t id = 0;
452 for (int i = 3; i >= 0; i--)
453 {
454 uint8_t hi = (uint8_t)(body[i] >> 4);
455 uint8_t lo = (uint8_t)(body[i] & 0x0Fu);
456 if (hi > 9 || lo > 9) // the identification number must be valid BCD
457 {
458 return false;
459 }
460 id = id * 100u + (uint32_t)(hi * 10u + lo);
461 }
462 out->id = id;
463 // Manufacturer: a 15-bit value packing three letters, each (n + 64) => 'A'..'Z' (EN 13757-3 ยง6.3.1).
464 uint16_t man = (uint16_t)(body[4] | (body[5] << 8));
465 out->manufacturer_raw = man;
466 out->manufacturer[0] = (char)(((man >> 10) & 0x1Fu) + 64u);
467 out->manufacturer[1] = (char)(((man >> 5) & 0x1Fu) + 64u);
468 out->manufacturer[2] = (char)((man & 0x1Fu) + 64u);
469 out->manufacturer[3] = '\0';
470 out->version = body[6];
471 out->medium = body[7];
472 out->access_no = body[8];
473 out->status = body[9];
474 out->signature = (uint16_t)(body[10] | (body[11] << 8));
475 return true;
476}
477
478#endif // PC_ENABLE_MBUS
Wired M-Bus (Meter-Bus, EN 13757-2/-3) frame codec (PC_ENABLE_MBUS).