Skip to content

Commit 574d93d

Browse files
committed
Docs update
1 parent 6abe712 commit 574d93d

File tree

4 files changed

+71
-9
lines changed

4 files changed

+71
-9
lines changed

ARCHITECTURE.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,31 @@ UART → check_frame() → COMPLETE / NEED_MORE / ERROR
6767
| **Logger** | Static logging interface, silent by default |
6868
| **utils** | Value conversion, OBIS/datetime formatting, type helpers |
6969

70+
## Zero-Heap Architecture
71+
72+
All parsing transforms happen in a single caller-owned work buffer. **No heap
73+
allocation occurs during `parse()`.** The caller provides the buffer at setup:
74+
75+
```cpp
76+
uint8_t work_buf[1024];
77+
parser.set_work_buffer(work_buf, sizeof(work_buf));
78+
```
79+
80+
The pipeline copies input into the work buffer, then transforms in-place:
81+
82+
```
83+
work_buf: [7E HDLC frames 7E] ← memcpy from input
84+
↓ HDLC decode (strip framing, concatenate payloads)
85+
work_buf: [E0 GBT blocks]
86+
↓ GBT reassembly (strip block headers)
87+
work_buf: [DB ciphertext]
88+
↓ AES-GCM decrypt in-place
89+
work_buf: [0F DATA-NOTIFICATION AXDR...]
90+
↓ strip APDU header → AxdrParser reads directly
91+
```
92+
93+
Each stage produces output ≤ input, so the buffer never grows.
94+
7095
## Caller vs Library Responsibilities
7196

7297
The library is **stateless** between calls — it does not buffer or accumulate data.
@@ -76,9 +101,10 @@ The library is **stateless** between calls — it does not buffer or accumulate
76101
| Reading bytes from UART | Caller |
77102
| Detecting frame boundaries (0x7E / 0x16) | Caller |
78103
| Accumulating multi-frame messages | Caller (using `check_frame()` to know when done) |
79-
| Decoding transport framing (HDLC / M-Bus) | Library |
80-
| Reassembling GBT blocks | Library |
81-
| Decrypting AES-GCM | Library |
104+
| Providing a work buffer (`set_work_buffer()`) | Caller |
105+
| Decoding transport framing (HDLC / M-Bus) | Library (in-place) |
106+
| Reassembling GBT blocks | Library (in-place) |
107+
| Decrypting AES-GCM | Library (in-place) |
82108
| Walking AXDR structure and matching patterns | Library |
83109
| Delivering parsed values via callbacks | Library |
84110

HOWTO.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,33 @@ parser.set_skip_crc_check(true);
8787

8888
This affects HDLC and M-Bus only. It has no effect in `RAW` mode.
8989

90+
## Providing A Work Buffer
91+
92+
The parser performs all transforms (frame decoding, GBT reassembly, decryption,
93+
APDU unwrapping) in a single caller-owned work buffer. **No heap allocation occurs
94+
during `parse()`.**
95+
96+
```cpp
97+
uint8_t work_buf[1024]; // stack, static, or PSRAM — caller controls placement
98+
parser.set_work_buffer(work_buf, sizeof(work_buf));
99+
```
100+
101+
The work buffer must be at least as large as the biggest raw frame the meter sends.
102+
If no work buffer is set, or the frame exceeds its capacity, `parse()` returns
103+
`{0, 0}` and logs an error.
104+
105+
Typical sizes:
106+
107+
| Scenario | Recommended size |
108+
|---|---|
109+
| Unencrypted single-frame HDLC | 256–512 bytes |
110+
| Encrypted single-frame M-Bus | 512 bytes |
111+
| Multi-frame HDLC or GBT (e.g. Landis+Gyr E450) | 1024 bytes |
112+
113+
The input buffer passed to `parse()` is **not modified** — data is copied into the
114+
work buffer first, then transformed in-place through each pipeline stage. Each stage
115+
produces output that is equal or smaller in size, so the buffer never grows.
116+
90117
## Accumulating Frames
91118

92119
Some meters split a single DLMS message across multiple transport frames (HDLC
@@ -311,9 +338,11 @@ ESPHome-style integration:
311338

312339
class MyMeterComponent {
313340
dlms_parser::DlmsParser parser_;
341+
uint8_t work_buf_[1024]{};
314342

315343
public:
316344
void setup() {
345+
parser_.set_work_buffer(work_buf_, sizeof(work_buf_));
317346
parser_.load_default_patterns();
318347
parser_.set_frame_format(dlms_parser::FrameFormat::HDLC);
319348

@@ -353,6 +382,8 @@ Examples of meter-specific customization from the test suite:
353382
354383
| Symptom | Likely cause |
355384
|---|---|
385+
| `No work buffer set` error | call `set_work_buffer()` before `parse()` |
386+
| `Frame too large for work buffer` | increase work buffer size |
356387
| `parse()` returns 0 | no patterns loaded |
357388
| `parse()` returns 0 with patterns loaded | no pattern matched the AXDR layout |
358389
| `HCS error` or `FCS error` | wrong frame format, damaged frame, or non-standard CRC |

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ It is designed for embedded and integration-heavy environments such as ESPHome,
1919
```cpp
2020
#include "dlms_parser/dlms_parser.h"
2121

22+
uint8_t work_buf[1024]; // caller-owned, no heap allocation during parse()
23+
2224
dlms_parser::DlmsParser parser;
25+
parser.set_work_buffer(work_buf, sizeof(work_buf));
2326
parser.set_frame_format(dlms_parser::FrameFormat::RAW);
2427
parser.load_default_patterns();
2528

@@ -31,7 +34,7 @@ auto on_value = [](const char* obis, float num, const char* str, bool is_numeric
3134
}
3235
};
3336

34-
size_t count = parser.parse(frame_bytes, frame_len, on_value);
37+
auto [count, consumed] = parser.parse(frame_bytes, frame_len, on_value);
3538
printf("%zu objects found\n", count);
3639
```
3740
@@ -54,11 +57,12 @@ parser.set_decryption_key(key);
5457
## Typical Usage Flow
5558

5659
1. Create `dlms_parser::DlmsParser`
57-
2. Select the frame format
58-
3. Set the decryption key if the meter is encrypted
59-
4. Load built-in patterns and optionally register custom ones
60-
5. Pass one complete frame to `parse()`
61-
6. Consume extracted values in the callback
60+
2. Provide a work buffer (`set_work_buffer`)
61+
3. Select the frame format
62+
4. Set the decryption key if the meter is encrypted
63+
5. Load built-in patterns and optionally register custom ones
64+
6. Pass one complete frame to `parse()`
65+
7. Consume extracted values in the callback
6266

6367
## Documentation
6468

REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Main facade that composes frame decoding, APDU handling, decryption, and AXDR pa
1111
| Method | Description |
1212
|---|---|
1313
| `set_frame_format(FrameFormat)` | Select transport wrapper: `RAW`, `HDLC`, or `MBUS` |
14+
| `set_work_buffer(buf, capacity)` | Provide a caller-owned buffer for in-place transforms — **required before `parse()`** |
1415
| `set_skip_crc_check(bool)` | Skip CRC/checksum validation for HDLC and M-Bus |
1516
| `set_decryption_key(std::array<uint8_t, 16>)` | Set AES-128-GCM key from a fixed array |
1617
| `set_decryption_key(std::vector<uint8_t>)` | Set AES-128-GCM key from a vector of exactly 16 bytes |

0 commit comments

Comments
 (0)