Examples

CRUMBS examples are organized into a three-tier learning path from basics to production patterns to real-world applications.

Three-Tier Structure

Tier 1: Core Usage (examples/core_usage/)

Audience: New users learning CRUMBS fundamentals
Purpose: Understand protocol, encoding, I2C communication

Start here: Learn message structure, encoding/decoding, basic request-reply patterns.

Tier 2: Handler Usage (examples/handlers_usage/)

Audience: Users ready for production callback patterns
Purpose: Learn handler registration and SET_REPLY query mechanism

Platform Examples
PlatformIO mock_peripheral
  mock_controller
Linux mock_controller

Key concepts:

  • Handler registration with crumbs_register_handler()
  • SET_REPLY pattern for querying data
  • on_request callback for GET operations

See handlers_usage/README.md for detailed handler pattern documentation.

Tier 3: Usage Families (examples/families_usage/)

Audience: Users building multi-device I²C systems
Purpose: Complete device family implementations with canonical operation headers

Family Devices Controllers Purpose
LHWIT Calculator (0x03)
LED Array (0x01)
Servo (0x02)
Display (0x04)
Discovery Controller
Manual Controller
Low Hardware Implementation Test - demonstrates function-style, state-query, position-control, and display patterns

Key concepts:

  • Canonical operation headers shared between peripherals and controllers
  • Multi-device coordination on single I²C bus
  • Discovery vs manual addressing patterns
  • Four interaction patterns: function-style (Calculator), state-query (LED), position-control (Servo), display-control (Display)

See families_usage/README.md for overview and lhwit-family.md for comprehensive guide.


Learning Path

  1. Start with Tier 1: Understand protocol basics
  2. Move to Tier 2: Learn production patterns
  3. Apply in Tier 3: Build multi-device systems

Platform Coverage

Platform Peripheral Examples Controller Examples
Arduino 3 Core 2 Core
PlatformIO 1 Core + 1 Handler + 3 Family 1 Core + 1 Handler
Linux — 1 Core + 1 Handler + 2 Family

Core Patterns

Controller Example (Simple Pattern)

Send commands via serial interface and request data from peripherals:

Usage

  1. Upload to Arduino
  2. Open Serial Monitor (115200 baud)
  3. Send: address,type_id,opcode,byte0,byte1,... (comma-separated bytes)
  4. Request: request=address

Example Commands

8,1,1,0,0,128,63,0,0,0,0     // Send to address 8 (first 4 bytes = 1.0f in little-endian)
request=8                    // Request data from address 8

See simple_controller and simple_peripheral for complete examples.


Handler-Based Peripheral (Production Pattern)

Instead of using a switch statement inside on_message, register individual handler functions for each command type. This pattern is cleaner when you have many commands.

Arduino Peripheral with Handlers

#include <crumbs.h>
#include <crumbs_arduino.h>

#define CMD_ECHO   0x01
#define CMD_PRINT  0x02
#define CMD_TOGGLE 0x03

static crumbs_context_t ctx;

void handler_echo(crumbs_context_t *ctx, uint8_t cmd,
                  const uint8_t *data, uint8_t len, void *user) {
    // Store received data for later retrieval
    memcpy(g_echo_buffer, data, len);
    g_echo_len = len;
}

void handler_print(crumbs_context_t *ctx, uint8_t cmd,
                   const uint8_t *data, uint8_t len, void *user) {
    // Print message to serial
    for (uint8_t i = 0; i < len; i++) {
        Serial.write(data[i]);
    }
    Serial.println();
}

void handler_toggle(crumbs_context_t *ctx, uint8_t cmd,
                    const uint8_t *data, uint8_t len, void *user) {
    // Toggle state
    g_state = !g_state;
}

void setup() {
    crumbs_arduino_init_peripheral(&ctx, 0x08);

    // Register per-command handlers
    crumbs_register_handler(&ctx, CMD_ECHO,   handler_echo,   NULL);
    crumbs_register_handler(&ctx, CMD_PRINT,  handler_print,  NULL);
    crumbs_register_handler(&ctx, CMD_TOGGLE, handler_toggle, NULL);

    // Set on_request callback for GET operations
    crumbs_set_callbacks(&ctx, NULL, on_request, NULL);
}

void loop() { }

See handlers_usage for complete working examples.


SET_REPLY Query Pattern (v0.10.0)

The SET_REPLY mechanism allows controllers to request specific data from peripherals. The library handles 0xFE automatically and stores the target opcode in ctx->requested_opcode.

Peripheral with SET_REPLY

#include <crumbs.h>
#include <crumbs_arduino.h>
#include <crumbs_message_helpers.h>

#define MY_TYPE_ID       0x42
#define MODULE_VER_MAJ   1
#define MODULE_VER_MIN   0
#define MODULE_VER_PAT   0

static crumbs_context_t ctx;
static uint16_t sensor_value = 0;

void on_request(crumbs_context_t *ctx, crumbs_message_t *reply)
{
    switch (ctx->requested_opcode)
    {
        case 0x00:  // Default: device/version info
            crumbs_msg_init(reply, MY_TYPE_ID, 0x00);
            crumbs_msg_add_u16(reply, CRUMBS_VERSION);
            crumbs_msg_add_u8(reply, MODULE_VER_MAJ);
            crumbs_msg_add_u8(reply, MODULE_VER_MIN);
            crumbs_msg_add_u8(reply, MODULE_VER_PAT);
            break;

        case 0x10:  // Sensor reading
            crumbs_msg_init(reply, MY_TYPE_ID, 0x10);
            crumbs_msg_add_u16(reply, sensor_value);
            break;

        default:  // Unknown opcode
            crumbs_msg_init(reply, MY_TYPE_ID, ctx->requested_opcode);
            break;
    }
}

void setup() {
    crumbs_arduino_init_peripheral(&ctx, 0x10);
    crumbs_set_callbacks(&ctx, NULL, on_request, NULL);
}

void loop() {
    sensor_value = analogRead(A0);  // Update sensor reading
    delay(100);
}

Controller Querying Sensor Data

#include <crumbs.h>
#include <crumbs_message_helpers.h>

// Step 1: Send SET_REPLY to request sensor data (opcode 0x10)
crumbs_message_t set_reply;
crumbs_msg_init(&set_reply, 0, CRUMBS_CMD_SET_REPLY);
crumbs_msg_add_u8(&set_reply, 0x10);  // target opcode
crumbs_controller_send(&ctx, 0x10, &set_reply, write_fn, io_ctx);

// Step 2: Read the response
uint8_t buf[CRUMBS_MESSAGE_MAX_SIZE];
int n = read_fn(io_ctx, 0x10, buf, sizeof(buf), 50000);
if (n >= 4)
{
    crumbs_message_t reply;
    if (crumbs_decode_message(buf, n, &reply, NULL) == 0)
    {
        uint16_t sensor;
        crumbs_msg_read_u16(reply.data, reply.data_len, 0, &sensor);
        printf("Sensor value: %u\n", sensor);
    }
}

See handlers_usage for complete SET_REPLY examples with mock device.


Message Helpers Pattern

The crumbs_message_helpers.h header provides type-safe payload building and reading. This pattern is cleaner than manual byte manipulation:

Controller with Message Helpers

#include <crumbs_message_helpers.h>

// Define command header (see examples/handlers_usage/mock_ops.h for full pattern)
#define SERVO_TYPE_ID    0x02
#define SERVO_CMD_ANGLE  0x01

crumbs_message_t msg;
crumbs_msg_init(&msg, SERVO_TYPE_ID, SERVO_CMD_ANGLE);
crumbs_msg_add_u8(&msg, servo_index);    // Which servo
crumbs_msg_add_u16(&msg, 1500);          // Pulse width in μs

crumbs_controller_send(&ctx, 0x10, &msg, write_fn, write_ctx);

Peripheral Handler with Message Readers

void handle_servo_angle(crumbs_context_t *ctx, uint8_t cmd,
                        const uint8_t *data, uint8_t len, void *user) {
    uint8_t index;
    uint16_t pulse;

    if (crumbs_msg_read_u8(data, len, 0, &index) < 0) return;
    if (crumbs_msg_read_u16(data, len, 1, &pulse) < 0) return;

    servo_set_pulse(index, pulse);
}

void setup() {
    crumbs_arduino_init_peripheral(&ctx, 0x10);
    crumbs_register_handler(&ctx, SERVO_CMD_ANGLE, handle_servo_angle, NULL);
}

See API Reference - Message Helpers for complete API documentation.


Common Patterns

Device Proxy Pattern (Controller-Side)

When sending many commands to the same device, you can bundle the context, address, and I/O function into a struct to reduce repetition:

// Bundle device parameters (user-side pattern, not library API)
typedef struct {
    crumbs_context_t *ctx;
    uint8_t addr;
    crumbs_i2c_write_fn write_fn;
    void *io;
} led_device_t;

// Initialize once
led_device_t led = { &ctx, 0x08, crumbs_arduino_wire_write, NULL };

// Shorter sender wrapper
static inline int led_set_all(led_device_t *dev, uint8_t bitmask) {
    crumbs_message_t msg;
    crumbs_msg_init(&msg, LED_TYPE_ID, LED_CMD_SET_ALL);
    crumbs_msg_add_u8(&msg, bitmask);
    return crumbs_controller_send(dev->ctx, dev->addr, &msg, dev->write_fn, dev->io);
}

// Clean usage
led_set_all(&led, 0x0F);

This is an optional convenience pattern. The library’s explicit parameter passing remains the primitive for maximum flexibility.

Multiple Devices

uint8_t addresses[] = {0x08, 0x09, 0x0A};
for (int i = 0; i < 3; i++) {
    crumbs_controller_send(&ctx, addresses[i], &msg, crumbs_arduino_wire_write, NULL);
    delay(10);
}

Data Requests

On Arduino, use Wire.requestFrom() and the core API helper to decode; e.g. read bytes and call crumbs_decode_message().

On Linux, the HAL helper crumbs_linux_read_message() wraps the low-level reads and decoding for you.

CRC Validation

Use crumbs_decode_message(buffer, bytesRead, &out, ctx) which returns 0 on success, -1 if too small and -2 on CRC mismatch.

I2C Scanning

for (uint8_t addr = 8; addr < 120; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
        Serial.print("Found device at 0x");
        Serial.println(addr, HEX);
    }
}

For protocol-aware discovery (find devices that actually speak CRUMBS) use the core helper crumbs_controller_scan_for_crumbs() which performs a read-and-decode probe. See the Getting Started guide for short Arduino and Linux examples.


Build Examples

CMake (Linux)

Linux examples use CMake for building. Example structure:

mkdir build && cd build
cmake .. -DCRUMBS_BUILD_IN_TREE=ON
make

See simple_controller for a complete CMake example.

PlatformIO

PlatformIO examples support multiple boards (Nano, ESP32):

cd examples/core_usage/platformio/simple_controller
pio run -e nanoatmega328new
pio run -e esp32dev

See PlatformIO examples for complete projects.