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.
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.
- Bring up your hardware as normal with Arduino or ESP-IDF.
- Install Courier (see below; we recommend managing your libraries with PlatformIO).
- Initialize Courier with a config struct, set up your callbacks, and call
setup()andloop().
#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(); }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.
- 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), plussend()/onMessagefor 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
typefield - Transport map — named transports, per-transport hooks for direct access
Courier bundles a number of other great libraries:
- WebSocket — esp_websocket_client Documentation
- MQTT — esp_mqtt_client Documentation
- WiFi config — WiFiManager GitHub
- JSON — ArduinoJson Documentation
- Time — ezTime GitHub
Use onConfigure hooks to access the full configuration surface of each bundled library.
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.
From GitHub (recommended while in active development):
lib_deps = https://github.com/inanimate-tech/courier.gitOr 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.2From GitHub (recommended while in active development):
dependencies:
inanimate-tech/courier:
git: https://github.com/inanimate-tech/courier.gitOr 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"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) { });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").
- Single instance — WiFiManager requires a static callback, so only one
Courier::Clientinstance 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
MIT