Layer: L6 Presentation · Build flags: PC_ENABLE_MSGPACK
What this example teaches
This is the CBOR example in a different wire format, in both directions: GET /telemetry.msgpack encodes the same {heap, uptime, rssi} map with the zero-heap MessagePack writer and streams it as application/msgpack, and POST /decode parses a posted MessagePack map with the zero-heap cursor decoder. MessagePack is widely supported across languages, so pick it over CBOR when your consuming stack already speaks it - the API and the zero-heap pattern are identical.
Encoding with MsgpackWriter. Initialize over a stack buffer, declare the map size, emit pairs, check pc_msgpack_ok(), write pc_msgpack_len() bytes:
uint8_t buf[64];
MsgpackWriter w;
pc_msgpack_init(&w, buf, sizeof(buf));
pc_msgpack_map(&w, 3);
pc_msgpack_str(&w, "heap"); pc_msgpack_uint(&w, ESP.getFreeHeap());
pc_msgpack_str(&w, "uptime"); pc_msgpack_uint(&w, millis() / 1000);
pc_msgpack_str(&w,
"rssi"); pc_msgpack_int(&w,
pc_net_rssi());
ctx.len = pc_msgpack_ok(&w) ? pc_msgpack_len(&w) : 0;
int8_t pc_net_rssi(void)
Station link RSSI in dBm, or 0 if not associated (and on host builds).
As with CBOR, the payload is binary so it is delivered through the binary-safe send_chunked(), which pulls the encoded bytes from a ChunkSource generator (slice by slice, returning 0 to finish) rather than the C-string send(). The generator's ctx must outlive the call, so it is static.
Decoding with MsgpackReader. The cursor decoder is the mirror image: bind a reader to the bytes, read the map header, then each key/value, and check pc_msgpack_reader_ok() once at the end (it is sticky, so a single check covers the whole parse). Strings point straight into the source buffer, no copy:
MsgpackReader r;
pc_msgpack_reader_init(&r, req->body, req->body_len);
size_t count;
if (!pc_msgpack_read_map(&r, &count)) { }
for (size_t i = 0; i < count && pc_msgpack_reader_ok(&r); i++) {
const char *key; size_t klen; int64_t val;
if (!pc_msgpack_read_str(&r, &key, &klen) || !pc_msgpack_read_int(&r, &val)) break;
}
if (!pc_msgpack_reader_ok(&r)) { }
Every read is bounds-checked, so malformed or truncated input fails closed rather than over-reading. pc_msgpack_peek() reports the next object's type if you need to branch on it.
Build and run
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_MSGPACK=1" \
--lib="." examples/L6-Presentation/MsgPack/MsgPack.ino
curl -s http://<ip>/telemetry.msgpack | xxd
# decode side: post a one-pair map {"led": 1} (0x81 0xa3 'led' 0x01)
printf '\x81\xa3led\x01' | curl -s --data-binary @- http://<ip>/decode
Annotated source
The complete sketch (MsgPack.ino), reproduced verbatim with added explanatory comments:
#define PC_ENABLE_MSGPACK 1
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
struct MpCtx
{
uint8_t buf[64];
size_t len, off;
};
static size_t pc_msgpack_source(uint8_t *out, size_t cap, void *vctx)
{
MpCtx *c = (MpCtx *)vctx;
if (c->off >= c->len)
return 0;
size_t n = c->len - c->off;
if (n > cap)
n = cap;
memcpy(out, c->buf + c->off, n);
c->off += n;
return n;
}
static void on_decode(uint8_t
id,
HttpReq *req)
{
MsgpackReader r;
size_t count;
if (!pc_msgpack_read_map(&r, &count))
{
server.
send(
id, 400,
"text/plain",
"expected a MessagePack map");
return;
}
char out[160];
size_t o = 0;
for (size_t i = 0; i < count && pc_msgpack_reader_ok(&r); i++)
{
const char *key;
size_t klen;
int64_t val;
if (!pc_msgpack_read_str(&r, &key, &klen) || !pc_msgpack_read_int(&r, &val))
break;
o += snprintf(out + o, sizeof(out) - o, "%.*s=%lld\n", (int)klen, key, (long long)val);
}
if (!pc_msgpack_reader_ok(&r))
{
server.
send(
id, 400,
"text/plain",
"malformed MessagePack");
return;
}
server.
send(
id, 200,
"text/plain", out);
}
void setup()
{
Serial.begin(115200);
Serial.print("Connecting to WiFi");
{
delay(250);
Serial.print('.');
}
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
static MpCtx ctx;
MsgpackWriter w;
pc_msgpack_init(&w, ctx.buf, sizeof(ctx.buf));
pc_msgpack_map(&w, 3);
pc_msgpack_str(&w, "heap");
pc_msgpack_uint(&w, ESP.getFreeHeap());
pc_msgpack_str(&w, "uptime");
pc_msgpack_uint(&w, millis() / 1000);
pc_msgpack_str(&w, "rssi");
ctx.len = pc_msgpack_ok(&w) ? pc_msgpack_len(&w) : 0;
ctx.off = 0;
server.
send_chunked(
id, 200,
"application/msgpack", pc_msgpack_source, &ctx);
});
}
void loop()
{
}
Single-port HTTP server with deterministic, zero-allocation execution.
void send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx=nullptr)
Stream a response body of unknown length via chunked transfer.
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server - call every Arduino loop() iteration.
Layer 6 (Presentation) - zero-heap MessagePack encoder and decoder.
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Layer 7 (Application) - public HTTP routing API.
@ HTTP_POST
Non-idempotent create / action.
@ HTTP_GET
Safe, idempotent read.
Fully-parsed HTTP/1.1 request.
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).