Message Helpers
The crumbs_msg.h header provides zero-overhead inline helpers for building and reading message payloads. These helpers eliminate manual byte manipulation when working with multi-byte integers and floats.
Overview
Instead of manually encoding values into msg.data[]:
// Manual approach (error-prone)
msg.data[0] = angle & 0xFF;
msg.data[1] = (angle >> 8) & 0xFF;
msg.data_len = 2;
Use the message helpers:
// With helpers (type-safe, bounds-checked)
#include "crumbs_msg.h"
crumbs_msg_init(&msg);
crumbs_msg_add_u16(&msg, angle);
Including the Header
#include "crumbs_msg.h" // Linux/CMake projects
#include <crumbs_msg.h> // Arduino projects
The header is standalone and only depends on <stdint.h>, <stddef.h>, and <string.h>.
Building Messages
Initialization
Always initialize a message before adding data:
crumbs_message_t msg;
crumbs_msg_init(&msg);
// Now set type_id and command_type
msg.type_id = MY_TYPE_ID;
msg.command_type = MY_COMMAND;
crumbs_msg_init() zeros the message and sets data_len = 0.
Adding Values
All add functions return 0 on success, -1 if the value would overflow the 27-byte payload limit.
// Unsigned integers (little-endian encoding)
crumbs_msg_add_u8(&msg, value); // 1 byte
crumbs_msg_add_u16(&msg, value); // 2 bytes
crumbs_msg_add_u32(&msg, value); // 4 bytes
// Signed integers (little-endian encoding)
crumbs_msg_add_i8(&msg, value); // 1 byte
crumbs_msg_add_i16(&msg, value); // 2 bytes
crumbs_msg_add_i32(&msg, value); // 4 bytes
// Floating point (native byte order - see notes)
crumbs_msg_add_float(&msg, value); // 4 bytes
// Raw bytes
crumbs_msg_add_bytes(&msg, ptr, len);
Example: Building a Servo Command
crumbs_message_t msg;
crumbs_msg_init(&msg);
msg.type_id = 0x02; // Servo device type
msg.command_type = 0x02; // Set both servos
crumbs_msg_add_u16(&msg, 1500); // Servo 1: 1500μs
crumbs_msg_add_u16(&msg, 2000); // Servo 2: 2000μs
// msg.data_len is now 4
Reading Payloads
Basic Pattern
Use an offset variable to track position while reading:
void my_handler(crumbs_context_t *ctx, uint8_t cmd,
const uint8_t *data, uint8_t len, void *user) {
size_t off = 0;
uint16_t servo1, servo2;
if (crumbs_msg_read_u16(data, len, &off, &servo1) < 0) return;
if (crumbs_msg_read_u16(data, len, &off, &servo2) < 0) return;
// Use servo1, servo2...
}
Reading Functions
All read functions return 0 on success, -1 if reading would exceed the buffer bounds.
// Unsigned integers
crumbs_msg_read_u8(data, len, &offset, &out);
crumbs_msg_read_u16(data, len, &offset, &out);
crumbs_msg_read_u32(data, len, &offset, &out);
// Signed integers
crumbs_msg_read_i8(data, len, &offset, &out);
crumbs_msg_read_i16(data, len, &offset, &out);
crumbs_msg_read_i32(data, len, &offset, &out);
// Floating point
crumbs_msg_read_float(data, len, &offset, &out);
// Raw bytes
crumbs_msg_read_bytes(data, len, &offset, dest, count);
Example: Reading LED Command
void handle_led_set_one(crumbs_context_t *ctx, uint8_t cmd,
const uint8_t *data, uint8_t len, void *user) {
size_t off = 0;
uint8_t led_index, r, g, b;
if (crumbs_msg_read_u8(data, len, &off, &led_index) < 0) return;
if (crumbs_msg_read_u8(data, len, &off, &r) < 0) return;
if (crumbs_msg_read_u8(data, len, &off, &g) < 0) return;
if (crumbs_msg_read_u8(data, len, &off, &b) < 0) return;
set_led(led_index, r, g, b);
}
Creating Command Headers
The recommended pattern is to create a command header file for each device type. See examples/common/ for complete examples.
Pattern
// my_device_commands.h
#ifndef MY_DEVICE_COMMANDS_H
#define MY_DEVICE_COMMANDS_H
#include "crumbs.h"
#include "crumbs_msg.h"
#define MY_DEVICE_TYPE_ID 0x10
// Command types
#define MY_CMD_ACTION_A 0x01
#define MY_CMD_ACTION_B 0x02
// Sender functions
static inline int my_send_action_a(crumbs_context_t *ctx, uint8_t addr,
uint16_t param1, uint8_t param2,
crumbs_i2c_write_fn write_fn, void *write_ctx) {
crumbs_message_t msg;
crumbs_msg_init(&msg);
msg.type_id = MY_DEVICE_TYPE_ID;
msg.command_type = MY_CMD_ACTION_A;
crumbs_msg_add_u16(&msg, param1);
crumbs_msg_add_u8(&msg, param2);
return crumbs_controller_send(ctx, addr, &msg, write_fn, write_ctx);
}
#endif
Benefits of Command Headers
- Type-safe: Function parameters enforce correct types
- Self-documenting: Command names and parameters are explicit
- Reusable: Same header works on controller and peripheral
- Composable: Include multiple device headers in one controller
Notes on Encoding
Integer Encoding
All multi-byte integers use little-endian encoding:
- LSB first, MSB last
- Matches x86, ARM, and most microcontrollers
- Consistent across platforms
Float Encoding
Floats use native byte order (memcpy):
- Works correctly when both endpoints have the same endianness
- Most embedded systems are little-endian (AVR, ARM Cortex-M, ESP32)
- Caution: If crossing to a big-endian system, floats may be misinterpreted
For maximum portability, consider encoding floats as fixed-point integers:
// Instead of float
crumbs_msg_add_float(&msg, 25.5f);
// Use fixed-point (e.g., tenths of a degree)
crumbs_msg_add_i16(&msg, 255); // 25.5 × 10
API Reference
Message Builder
| Function | Description | Bytes |
|---|---|---|
crumbs_msg_init(msg) |
Zero message, set data_len=0 | — |
crumbs_msg_add_u8(msg, v) |
Append uint8_t | 1 |
crumbs_msg_add_u16(msg, v) |
Append uint16_t (little-endian) | 2 |
crumbs_msg_add_u32(msg, v) |
Append uint32_t (little-endian) | 4 |
crumbs_msg_add_i8(msg, v) |
Append int8_t | 1 |
crumbs_msg_add_i16(msg, v) |
Append int16_t (little-endian) | 2 |
crumbs_msg_add_i32(msg, v) |
Append int32_t (little-endian) | 4 |
crumbs_msg_add_float(msg, v) |
Append float (native order) | 4 |
crumbs_msg_add_bytes(msg, p, n) |
Append n bytes from p | n |
Message Reader
| Function | Description | Bytes |
|---|---|---|
crumbs_msg_read_u8(d, len, &off, &out) |
Read uint8_t, advance offset | 1 |
crumbs_msg_read_u16(d, len, &off, &out) |
Read uint16_t (little-endian) | 2 |
crumbs_msg_read_u32(d, len, &off, &out) |
Read uint32_t (little-endian) | 4 |
crumbs_msg_read_i8(d, len, &off, &out) |
Read int8_t | 1 |
crumbs_msg_read_i16(d, len, &off, &out) |
Read int16_t (little-endian) | 2 |
crumbs_msg_read_i32(d, len, &off, &out) |
Read int32_t (little-endian) | 4 |
crumbs_msg_read_float(d, len, &off, &out) |
Read float (native order) | 4 |
crumbs_msg_read_bytes(d, len, &off, dest, n) |
Read n bytes into dest | n |
All functions return 0 on success, -1 on bounds error.
See Also
- API Reference — Core CRUMBS API
- Examples — Complete usage examples
- Protocol — Wire format specification