FirmwareBy Alison Alva••3 min read
Ant Buffers: A look into my ecosystem
A brief rundown of one of the internal building blocks I've built for my embedded designs. Deterministic device-agnostic buffer primitives I created to be utilized by my LogAnywhere library and Ant-Stack transport layers.
EmbeddedC++IoT
<hr><h2 id="why">Why?</h2><p>When you're working with npm, there's a library for everything. Venture into k8s land and when you encounter a problem there's often someone trying to make a solution for it. Step into embedded land and you're lucky if you can find useful libraries at all, and if you want them optimized and agnostic? Good luck. </p><p>I have found myself re-implementing code for a few things, one of the more annoying ones was particular buffers. Yeah, they're simple, they're not hard to recreate, but also, I really don't want to. I was building routing-heavy embedded firmware (logs + telemetry across multiple transports). I needed buffering that was:</p><ul><li>deterministic (no heap surprises),</li><li>small (AVR-class friendly),</li><li>device/SDK agnostic,</li><li>and predictable under load.</li></ul><p>There are some libraries that existed, but none that fit my particular needs of being device agnostic, not tied to a specific RTOS/SDK, and not relying on dynamic allocation.</p><p>So I wrote a few tiny headers I called antBuffers:</p><ol><li><strong>ByteBuffer</strong> - sequential read/write over a raw byte array, with endian helpers.</li><li><strong>MessageBuffer</strong> - minimal framed message builder/parser for packet links.</li><li><strong>RingBuffer</strong> - fixed-capacity circular queue for any type.</li></ol><p>They're boring, they're small, they work anywhere.</p><h2 id="1-bytebuffer-raw-bytes-less-footguns">1) ByteBuffer: raw bytes, less footguns</h2><p><strong>ByteBuffer</strong> is a non-owning view over a <code>uint8_t*</code> array. It keeps a write head and read tail so you can append and consume safely without manual index juggling.</p><p>Design goals:</p><ul><li>no allocation,</li><li>constant-time bounds checks,</li><li>explicit failure (read/write returns false on underflow/overflow),</li><li>endian helpers so packet code doesn't turn into shift soup.</li></ul><p>Typical uses for me:</p><ul><li>building payloads for RF/BLE packets,</li><li>parsing binary frames from peers/sensors,</li><li>walking a shared scratch buffer safely.</li></ul><p>Example pattern:</p><pre><code class="language-cpp">uint8_t storage[64];
antBuffers::ByteBuffer b(storage, sizeof(storage));
b.writeUInt8(0xAA);
b.writeUInt16LE(0x1234);
b.writeUInt32BE(0xCAFEBABE);
b.resetRead();
uint8_t a; uint16_t u16; uint32_t u32;
b.readUInt8(a);
b.readUInt16LE(u16);
b.readUInt32BE(u32);
</code></pre><h2 id="2-messagebuffer-tiny-framing-for-small-links">2) MessageBuffer: tiny framing for small links</h2><p><strong>MessageBuffer</strong> adds dumb-simple framing on top of a raw array:</p><ul><li>1 byte type</li><li>1 byte payload length</li><li>payload bytes after that</li></ul><p>You call <code>beginMessage(type)</code>, write bytes, then <code>finalizeMessage()</code> fills in the length field. On receive, you <code>beginRead(rxSize)</code> and iterate the payload.</p><p>This maps cleanly to embedded links where <em>framing matters</em> but overhead must stay low:</p><ul><li>serial streams,</li><li>BLE characteristics,</li><li>LPWAN payloads,</li><li>short-range mesh packets.</li></ul><p>Example send:</p><pre><code class="language-cpp">uint8_t msgStorage[64];
antBuffers::MessageBuffer m(msgStorage, sizeof(msgStorage));
m.beginMessage(0x02); // telemetry
m.writeByte(tempC);
m.writeByte(humidityPct);
m.finalizeMessage();
// send m.data(), m.size()
</code></pre><p>Example receive:</p><pre><code class="language-cpp">if (m.beginRead(rxSize)) {
uint8_t type = m.messageType();
while (m.readRemaining()) {
uint8_t v;
m.readByte(v);
// handle v
}
}
</code></pre><p>Not protobuf. Not CBOR. Just enough structure to keep links sane.</p><h2 id="3-ringbuffer-fixed-queue-zero-drama">3) RingBuffer: fixed queue, zero drama</h2><p><strong>RingBuffer<T, N></strong> is a compile-time circular queue:</p><ul><li><code>push()</code> fails when full,</li><li><code>pop()</code> fails when empty,</li><li>O(1) ops,</li><li>no heap,</li><li>no ownership surprises.</li></ul><p>Typical uses:</p><ul><li>buffering logs before a slow transport drains them,</li><li>decoupling ISR-ish producers from main-loop consumers,</li><li>small mailbox queues between subsystems.</li></ul><p>The capacity being a template parameter forces honest sizing up front.</p><h2 id="device-agnostic-by-choice">Device-agnostic by choice</h2><p>These headers do not depend on ESP-IDF, Arduino, Zephyr, or any RTOS primitives. They are plain C++11 + <code><cstdint></code>/<code><cstddef></code>.</p><p>That matters because I bounce between:</p><ul><li>small "prove it runs on AVR-class targets" builds,</li><li>larger RTOS MCU builds,</li><li>and host-side tests.</li></ul><p>Same buffers, different targets, no platform baggage.</p><h2 id="future">Future!</h2><p>Two future directions, if/when I need them:</p><ol><li><strong>Configurable stack limits</strong><br>Still static-only, but more knobs for buffer sizes where it makes sense.</li><li><strong>Optional NVM-backed ring</strong><br>For crash-survivable "last N logs" cases. This would be an adapter layer, not a change to core primitives (I want to keep agnosticism intact)</li></ol><h2 id="tldr">TL;DR</h2><p>I needed tiny, deterministic, device-agnostic buffering for routed logging and multi-transport firmware. Existing libs either didn't fit, or brought heap/platform baggage. So I wrote three small headers and moved on.</p><p>If you're doing embedded work where memory is a real constraint, these are the kind of boring building blocks that save you time later.</p><p><strong>GitHub:</strong> <a href="https://github.com/Allie-Leth/ant-buffer?ref=ghost.scopecreep.productions">github.com/Allie-Leth/ant-buffer</a></p>