Skip to content

Repository files navigation

courier

Batteries-included JSON messaging for ESP32. WiFi and user configuration, WebSocket, MQTT, UDP multicast, self-healing reconnection — all handled.

Important

Courier is under active development. See docs/changelog.md for changes on each release.

⚠️ This is v0.4.0 and the API has changed considerably.

To pin to the previous stable version use lib_deps = https://github.com/inanimate-tech/courier.git#v0.3.2 in your platformio.ini.

When you're ready to migrate, change your lib_deps to use the latest version from GitHub and point your coding agent at docs/migration-0.3-to-0.4.md.

Motivation: When you make something neat on your M5Stick you want the quickest path to messaging the back-end, and you want to carry it to places to show people and configure the Wi-Fi from your phone. Courier is how you do that.

Courier expects JSON messages with a "type" field. Messages are parsed with ArduinoJson and the type string is passed to the onMessage callback alongside the parsed document. Use the per-transport receive hooks (WebSocketTransport::onText, MqttTransport::onMessage, etc.) for non-JSON or topic-routed payloads.

Quick Start

  1. Bring up your hardware as normal with Arduino or ESP-IDF.
  2. Install Courier (see below; we recommend managing your libraries with PlatformIO).
  3. Initialize Courier with a config struct, set up your callbacks, and call setup() and loop().
#include <Courier.h>

Courier::Config makeConfig() {
  Courier::Config cfg;
  cfg.host = "api.example.com";
  cfg.port = 443;
  cfg.path = "/ws";
  cfg.defaultTransport = "ws";   // enable courier.send(doc)
  return cfg;
}

Courier::Client courier(makeConfig());

void setup() {
  courier.onConnected([]() {
    JsonDocument doc;
    doc["type"] = "hello";
    courier.send(doc);
  });
  courier.onMessage([](const char* transportName, const char* type, JsonDocument& doc) {
    Serial.printf("Got: %s (via %s)\n", type, transportName);
  });
  courier.setup();
}

void loop() { courier.loop(); }

HTTPS

HttpTransport gives you a JS-shaped fetch() alongside WiFi and time sync — useful on its own (no WS/MQTT) or as an extra transport. Set defaultTransport = "https" to skip auto-registering "ws":

#include <Courier.h>
#include <HttpTransport.h>

Courier::Config makeConfig() {
  Courier::Config cfg;
  cfg.host = "httpbin.org";
  cfg.port = 443;
  cfg.path = "/anything";          // send() POSTs here
  cfg.defaultTransport = "https";  // no built-in "ws"
  return cfg;
}

Courier::Client courier(makeConfig());

void setup() {
  auto& http = courier.addTransport<Courier::HttpTransport>("https");

  courier.onConnected([]() {
    auto& http = courier.transport<Courier::HttpTransport>("https");
    Courier::Response r = http.get("https://httpbin.org/get");
    Serial.printf("GET -> %d\n", r.status);
  });

  courier.setup();
}

void loop() { courier.loop(); }

See examples/https-only for the full sketch.

What it does

  • WiFi — captive portal config via WiFiManager, auto-reconnection
  • WebSocket — built-in transport with TLS, ping/pong heartbeat, self-healing auto-reconnect
  • MQTT — opt-in transport with subscribe/unsubscribe, topic-addressed publishing (text or NUL-safe binary), self-healing auto-reconnect
  • UDP multicast — opt-in transport for local network discovery and messaging
  • HTTPS — opt-in transport with a JS-shaped fetch() (buffered or streaming), plus send()/onMessage for the messaging idiom
  • Self-healing — transports auto-reconnect independently; if all persistent transports fail after 60s, Courier escalates to full WiFi reconnection
  • Reconnection — exponential backoff (5s-60s), health monitoring, automatic recovery
  • Time sync — NTP primary (continuous drift correction) + HTTP Date header fallback
  • JSON routing — messages parsed and dispatched by type field
  • Transport map — named transports, per-transport hooks for direct access

Opinionated

Courier bundles a number of other great libraries:

Use onConfigure hooks to access the full configuration surface of each bundled library.

Licenses

Courier itself is MIT. Its bundled dependencies carry their own (permissive) licenses:

  • WiFiManager, ArduinoJson, ezTime — MIT
  • esp_websocket_client, esp_mqtt_client, ESP-IDF — Apache 2.0
  • arduino-esp32 — LGPL 2.1+

When you ship firmware built with Courier, those libraries ship with it. Follow each library's notice/attribution requirements as applicable — in particular arduino-esp32's LGPL terms around relinking if you statically link it into a closed-source binary.

Install

PlatformIO

From GitHub (recommended while in active development):

lib_deps = https://github.com/inanimate-tech/courier.git

Or to pin a version: https://github.com/inanimate-tech/courier.git#v0.3.2

From the PlatformIO registry (for stable versions):

lib_deps = inanimate/courier@^0.3.2

ESP-IDF Component

From GitHub (recommended while in active development):

dependencies:
  inanimate-tech/courier:
    git: https://github.com/inanimate-tech/courier.git

Or to pin a version, add version: v0.3.2

From the ESP Component Registry (for stable versions):

dependencies:
  inanimate-tech/courier:
    version: "^0.3.2"

API

See docs/api.md for the full API reference. Migrating from 0.3.x? See docs/migration-0.3-to-0.4.md.

Quick overview:

// State
courier.isConnected();
courier.getState();        // Courier::State

// Sending via Client (routes to defaultTransport)
JsonDocument doc;
doc["type"] = "hello";
courier.send(doc);                          // WS default
Courier::SendOptions opts;
opts.topic = "sensors/me";
courier.send(doc, opts);                    // MQTT with per-call topic

// Explicit transport access — for raw frames or multi-transport setups
courier.transport<Courier::WebSocketTransport>("ws").sendText(payload);
courier.transport<Courier::WebSocketTransport>("ws").sendBinary(data, len);
courier.transport<Courier::MqttTransport>("mqtt").publish("topic", payload);
courier.transport<Courier::MqttTransport>("mqtt").publishBinary("topic", data, len);

// Transports — Client constructs and owns
auto& mqtt = courier.addTransport<Courier::MqttTransport>("mqtt", mqttCfg);
courier.suspend();   // free SRAM for OTA
courier.resume();

// Callbacks (single-slot, last registration wins)
courier.onMessage([](const char* transportName, const char* type, JsonDocument& doc) { });
courier.onConnected([]() { });
courier.onDisconnected([]() { });
courier.onError([](const char* category, const char* msg) { });

// Per-transport hooks (raw / topic-aware / binary)
auto& ws = courier.transport<Courier::WebSocketTransport>("ws");
ws.onText  ([](const char* p, size_t l)    { });
ws.onBinary([](const uint8_t* d, size_t l) { });
mqtt.onMessage([](const char* topic, const char* p, size_t l) { });
mqtt.subscribeBinary("topic/audio");   // declares the lane: bytes, never JSON
mqtt.onBinary([](const char* topic, const uint8_t* d, size_t l) { });
mqtt.onError([](const Courier::MqttTransport::ErrorInfo& e) { });  // CONNACK / TLS detail

// Raw ESP-IDF config access
ws.onConfigure  ([](esp_websocket_client_config_t& cfg) { });
mqtt.onConfigure([](esp_mqtt_client_config_t& cfg)      { });
courier.onConfigureWiFi([](WiFiManager& wm) { });

Connectivity state machine

Booting -> WifiConnecting -> WifiConnected -> TransportsConnecting -> Connected
                                                     ^                    |
                                                Reconnecting <-----------+
                                                     |
                                             ConnectionFailed

onConnectionChange fires at each state transition. onError fires alongside transitions caused by failures, providing a category and reason (e.g. "WIFI", "connection lost").

Key design constraints

  • Single instance — WiFiManager requires a static callback, so only one Courier::Client instance per process
  • Single-slot callbacks — each on* method is a setter (last registration wins). Application frameworks take the slot and expose virtual methods for subclasses
  • Bounded SPSC FIFO per transport — small bounded queue (depth 8) absorbs bursts; sustained overload drops
  • Arduino + ESP-IDF — depends on Arduino framework for WiFiManager, ArduinoJson, ezTime

License

MIT

About

Batteries-included ESP32 connectivity library — WiFi, WebSocket, MQTT, reconnection, all handled

Topics

Resources

Stars

54 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages