alteriom packages - Alteriom/painlessMesh GitHub Wiki
Alteriom provides production-ready, type-safe packages for painlessMesh that handle common IoT communication patterns. These packages eliminate the need for manual JSON handling and provide compile-time type safety.
For broadcasting environmental sensor data across the mesh network.
#include "examples/alteriom/alteriom_sensor_package.hpp"
using namespace alteriom;
SensorPackage sensor;
sensor.temperature = 23.5; // °C
sensor.humidity = 65.0; // %
sensor.pressure = 1013.25; // hPa
sensor.sensorId = 1001; // Unique sensor identifier
sensor.timestamp = mesh.getNodeTime();
sensor.batteryLevel = 85; // %
// Send to all nodes
mesh.sendPackage(&sensor);Fields:
-
temperature(double) - Temperature in Celsius -
humidity(double) - Relative humidity percentage -
pressure(double) - Atmospheric pressure in hPa -
sensorId(uint32_t) - Unique sensor identifier -
timestamp(uint32_t) - Measurement timestamp -
batteryLevel(uint8_t) - Battery level percentage
For sending control commands to specific devices in the mesh.
CommandPackage cmd;
cmd.dest = targetNodeId; // Target node ID
cmd.command = 1; // Command type (LED_CONTROL)
cmd.targetDevice = 100; // Device ID (LED strip)
cmd.parameters = "{\"brightness\":75,\"color\":\"blue\"}";
cmd.commandId = generateCommandId();
// Send to specific node
mesh.sendPackage(&cmd);Fields:
-
command(uint8_t) - Command type identifier -
targetDevice(uint32_t) - Target device ID -
commandId(uint32_t) - Unique command identifier for tracking -
parameters(TSTRING) - JSON parameters for the command
For broadcasting device health and operational status.
StatusPackage status;
status.deviceStatus = 1; // OPERATIONAL
status.uptime = millis() / 1000;
status.freeMemory = ESP.getFreeHeap();
status.wifiStrength = WiFi.RSSI();
status.firmwareVersion = "2.0.0";
// Broadcast to all nodes
mesh.sendPackage(&status);Fields:
-
deviceStatus(uint8_t) - Device status flags -
uptime(uint32_t) - Device uptime in seconds -
freeMemory(uint16_t) - Available memory in bytes -
wifiStrength(uint8_t) - WiFi signal strength -
firmwareVersion(TSTRING) - Firmware version string
Register handlers to process incoming packages:
void setup() {
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
// Register package handlers
mesh.onPackage(200, handleSensorData);
mesh.onPackage(400, handleCommand);
mesh.onPackage(202, handleStatus);
}
void handleSensorData(protocol::Variant& variant) {
SensorPackage sensor = variant.to<SensorPackage>();
Serial.printf("Sensor %u: T=%.1f°C, H=%.1f%%, P=%.1f hPa\n",
sensor.sensorId, sensor.temperature,
sensor.humidity, sensor.pressure);
// Process sensor data
if (sensor.temperature > 30.0) {
triggerCooling();
}
if (sensor.batteryLevel < 20) {
Serial.printf("Low battery warning: Node %u, Sensor %u\n",
sensor.from, sensor.sensorId);
}
}
void handleCommand(protocol::Variant& variant) {
CommandPackage cmd = variant.to<CommandPackage>();
// Check if command is for this node
if (cmd.dest != mesh.getNodeId()) {
return;
}
Serial.printf("Command %u for device %u: %u\n",
cmd.commandId, cmd.targetDevice, cmd.command);
// Execute command
executeDeviceCommand(cmd);
// Send acknowledgment
sendCommandAcknowledgment(cmd);
}
void handleStatus(protocol::Variant& variant) {
StatusPackage status = variant.to<StatusPackage>();
Serial.printf("Node %u status: %s, uptime %u seconds\n",
status.from,
status.deviceStatus == 1 ? "OK" : "ERROR",
status.uptime);
// Monitor network health
if (status.freeMemory < 10000) {
Serial.printf("Low memory warning: Node %u has %u bytes\n",
status.from, status.freeMemory);
}
}// ✅ Correct - compiler validates types
sensor.temperature = 25.5;
sensor.sensorId = 1001;
// ❌ Compiler error - type mismatch
sensor.temperature = "invalid";
sensor.sensorId = "not a number";Modern IDEs provide autocompletion and type hints:
sensor.| // IDE shows: temperature, humidity, pressure, sensorId, etc.No manual JSON handling required:
// Automatic conversion to/from JSON
mesh.sendPackage(&sensor); // Automatically serializes
// In handler
SensorPackage received = variant.to<SensorPackage>(); // Automatically deserializesDefine application-specific command constants:
enum DeviceCommands {
LED_CONTROL = 1,
SERVO_POSITION = 2,
RELAY_SWITCH = 3,
DISPLAY_UPDATE = 4,
SENSOR_CALIBRATION = 5
};
void executeDeviceCommand(const CommandPackage& cmd) {
switch(cmd.command) {
case LED_CONTROL:
handleLEDCommand(cmd);
break;
case SERVO_POSITION:
handleServoCommand(cmd);
break;
case RELAY_SWITCH:
handleRelayCommand(cmd);
break;
// ... other commands
}
}Use structured parameters for complex commands:
void handleLEDCommand(const CommandPackage& cmd) {
// Parse JSON parameters
DynamicJsonDocument doc(256);
deserializeJson(doc, cmd.parameters);
int brightness = doc["brightness"];
String color = doc["color"];
int duration = doc["duration"];
// Execute LED control
setLEDColor(color);
setLEDBrightness(brightness);
if (duration > 0) {
scheduleAutoOff(duration);
}
}Use bit flags for detailed device status:
enum StatusFlags {
STATUS_OK = 0x01,
STATUS_LOW_MEMORY = 0x02,
STATUS_WEAK_SIGNAL = 0x04,
STATUS_SENSOR_ERROR = 0x08,
STATUS_BATTERY_LOW = 0x10
};
void updateDeviceStatus() {
StatusPackage status;
status.from = mesh.getNodeId();
status.deviceStatus = STATUS_OK;
if (ESP.getFreeHeap() < 10000) {
status.deviceStatus |= STATUS_LOW_MEMORY;
}
if (WiFi.RSSI() < -80) {
status.deviceStatus |= STATUS_WEAK_SIGNAL;
}
if (batteryVoltage < 3.2) {
status.deviceStatus |= STATUS_BATTERY_LOW;
}
status.uptime = millis() / 1000;
status.freeMemory = ESP.getFreeHeap();
status.wifiStrength = WiFi.RSSI();
status.firmwareVersion = FIRMWARE_VERSION;
mesh.sendPackage(&status);
}Implement reliable command delivery:
void sendCommandAcknowledgment(const CommandPackage& originalCmd) {
CommandPackage ack;
ack.dest = originalCmd.from;
ack.command = 255; // ACK command
ack.targetDevice = originalCmd.targetDevice;
ack.commandId = originalCmd.commandId;
ack.parameters = "OK";
mesh.sendPackage(&ack);
}
// In command sender
std::map<uint32_t, uint32_t> pendingCommands; // commandId -> timestamp
void trackCommand(const CommandPackage& cmd) {
pendingCommands[cmd.commandId] = millis();
}
void checkCommandTimeouts() {
uint32_t now = millis();
for (auto it = pendingCommands.begin(); it != pendingCommands.end();) {
if (now - it->second > 5000) { // 5 second timeout
Serial.printf("Command %u timed out\n", it->first);
it = pendingCommands.erase(it);
} else {
++it;
}
}
}Packages automatically calculate required buffer sizes:
// Get exact buffer size needed
size_t bufferSize = sensor.jsonObjectSize();
DynamicJsonDocument doc(bufferSize);
// Or use static sizing for known maximums
StaticJsonDocument<256> doc; // Sufficient for most SensorPackagesTypical memory usage per package:
- SensorPackage: ~150-200 bytes
- CommandPackage: ~100-300 bytes (depending on parameters)
- StatusPackage: ~200-250 bytes
Packages use TSTRING for cross-platform string compatibility:
#ifdef ARDUINO
typedef String TSTRING; // Arduino String class
#else
typedef std::string TSTRING; // Standard C++ string
#endifCompatible with both ArduinoJson v6 and v7:
#if ARDUINOJSON_VERSION_MAJOR >= 7
// v7 automatic sizing
#else
// v6 manual sizing with jsonObjectSize()
#endifUnit tests ensure package reliability:
#include "catch2/catch.hpp"
#include "examples/alteriom/alteriom_sensor_package.hpp"
SCENARIO("SensorPackage serialization works correctly") {
GIVEN("A SensorPackage with test data") {
auto pkg = SensorPackage();
pkg.from = 12345;
pkg.temperature = 25.5;
pkg.humidity = 60.0;
pkg.pressure = 1013.25;
pkg.sensorId = 1001;
pkg.timestamp = 1234567890;
pkg.batteryLevel = 85;
WHEN("Converting to and from Variant") {
auto var = protocol::Variant(&pkg);
auto pkg2 = var.to<SensorPackage>();
THEN("All fields should match") {
REQUIRE(pkg2.from == pkg.from);
REQUIRE(pkg2.temperature == pkg.temperature);
REQUIRE(pkg2.humidity == pkg.humidity);
REQUIRE(pkg2.pressure == pkg.pressure);
REQUIRE(pkg2.sensorId == pkg.sensorId);
REQUIRE(pkg2.timestamp == pkg.timestamp);
REQUIRE(pkg2.batteryLevel == pkg.batteryLevel);
}
}
}
}Extend the Alteriom package system with your own types:
class WeatherPackage : public painlessmesh::plugin::BroadcastPackage {
public:
double windSpeed = 0.0;
double windDirection = 0.0;
double rainfall = 0.0;
uint32_t stationId = 0;
TSTRING location = "";
WeatherPackage() : BroadcastPackage(206) {} // Use an ID from the 206+ range
WeatherPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
windSpeed = jsonObj["windSpeed"];
windDirection = jsonObj["windDirection"];
rainfall = jsonObj["rainfall"];
stationId = jsonObj["stationId"];
location = jsonObj["location"].as<TSTRING>();
}
JsonObject addTo(JsonObject&& jsonObj) const {
jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
jsonObj["windSpeed"] = windSpeed;
jsonObj["windDirection"] = windDirection;
jsonObj["rainfall"] = rainfall;
jsonObj["stationId"] = stationId;
jsonObj["location"] = location;
return jsonObj;
}
#if ARDUINOJSON_VERSION_MAJOR < 7
size_t jsonObjectSize() const {
return JSON_OBJECT_SIZE(noJsonFields + 5) + location.length();
}
#endif
};- SensorPackage: Environmental readings, measurements
- CommandPackage: Device control, configuration changes
- StatusPackage: Health monitoring, diagnostics
- Custom packages: Domain-specific data structures
// Include timestamps for data freshness validation
sensor.timestamp = mesh.getNodeTime();
// Use unique IDs for command tracking
cmd.commandId = random(1000000, 9999999);
// Include version info for compatibility
status.firmwareVersion = "2.1.0";void handleSensorData(protocol::Variant& variant) {
try {
SensorPackage sensor = variant.to<SensorPackage>();
// Validate data ranges
if (sensor.temperature < -50 || sensor.temperature > 100) {
Serial.println("Invalid temperature reading");
return;
}
processSensorData(sensor);
} catch (const std::exception& e) {
Serial.printf("Error processing sensor package: %s\n", e.what());
}
}void handleSensorData(protocol::Variant& variant) {
uint32_t startTime = micros();
SensorPackage sensor = variant.to<SensorPackage>();
processSensorData(sensor);
uint32_t processingTime = micros() - startTime;
if (processingTime > 10000) { // > 10ms
Serial.printf("Slow sensor processing: %u µs\n", processingTime);
}
}For monitoring bridge health and Internet connectivity.
#include "examples/alteriom/alteriom_sensor_package.hpp"
using namespace alteriom;
BridgeStatusPackage bridgeStatus;
bridgeStatus.internetConnected = true;
bridgeStatus.routerRSSI = -45; // dBm
bridgeStatus.routerChannel = 6;
bridgeStatus.uptime = millis();
bridgeStatus.gatewayIP = "192.168.1.1";
bridgeStatus.timestamp = mesh.getNodeTime();
// Broadcast to all nodes
mesh.sendPackage(&bridgeStatus);Fields:
-
internetConnected(bool) - Bridge Internet connectivity status -
routerRSSI(int8_t) - Router WiFi signal strength in dBm -
routerChannel(uint8_t) - Router WiFi channel -
uptime(uint32_t) - Bridge uptime in milliseconds -
gatewayIP(TSTRING) - Router gateway IP address -
timestamp(uint32_t) - Status check timestamp
Use Cases:
- Bridge health monitoring
- Internet connectivity tracking
- Automatic failover detection
- Network diagnostics
For coordinating automatic bridge failover elections.
BridgeElectionPackage election;
election.routerRSSI = WiFi.RSSI(); // -127 to 0 dBm
election.routerChannel = WiFi.channel();
election.uptime = millis();
election.freeMemory = ESP.getFreeHeap();
election.timestamp = mesh.getNodeTime();
election.routerSSID = "MyRouter";
// Broadcast election candidacy
mesh.sendPackage(&election);Fields:
-
routerRSSI(int8_t) - Router WiFi signal strength (-127 to 0 dBm) -
routerChannel(uint8_t) - Router channel used if this candidate wins -
uptime(uint32_t) - Node uptime in milliseconds -
freeMemory(uint32_t) - Free memory in bytes -
timestamp(uint32_t) - Election timestamp -
routerSSID(TSTRING) - Router SSID for verification
Use Cases:
- Automatic bridge election
- Failover coordination
- RSSI-based selection
- Distributed consensus
For announcing bridge role transitions.
BridgeTakeoverPackage takeover;
takeover.previousBridge = oldBridgeNodeId; // 0 if none
takeover.reason = "Internet connectivity lost";
takeover.routerChannel = WiFi.channel();
takeover.timestamp = mesh.getNodeTime();
// Announce new bridge role
mesh.sendPackage(&takeover);Fields:
-
previousBridge(uint32_t) - Previous bridge node ID (0 if none) -
reason(TSTRING) - Takeover reason description -
routerChannel(uint8_t) - Channel peers must follow during takeover -
timestamp(uint32_t) - Takeover timestamp
Use Cases:
- Bridge transition announcements
- Failover notifications
- Network event logging
- Topology tracking
For distributing NTP time synchronization from bridge to mesh.
NTPTimeSyncPackage ntpSync;
ntpSync.ntpTime = 1699564800; // Unix timestamp
ntpSync.accuracy = 50; // ±50ms uncertainty
ntpSync.source = "pool.ntp.org";
ntpSync.timestamp = mesh.getNodeTime();
// Broadcast NTP time to mesh
mesh.sendPackage(&ntpSync);Fields:
-
ntpTime(uint32_t) - Unix timestamp from NTP server -
accuracy(uint16_t) - Milliseconds uncertainty/precision -
source(TSTRING) - NTP server source -
timestamp(uint32_t) - Collection timestamp
Use Cases:
- Time synchronization from Internet
- RTC backup coordination
- Mesh-wide time distribution
- Offline timekeeping
For coordinating multiple simultaneous bridges in advanced deployments.
BridgeCoordinationPackage coordination;
coordination.priority = 10; // Highest priority
coordination.role = "primary"; // "primary", "secondary", "standby"
coordination.peerBridges.push_back(bridge2NodeId);
coordination.peerBridges.push_back(bridge3NodeId);
coordination.load = 35; // 35% load
coordination.timestamp = mesh.getNodeTime();
// Broadcast to coordinate with other bridges
mesh.sendPackage(&coordination);// Monitor bridge coordination (fires every ~30s per bridge)
mesh.onBridgeCoordination(
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode) {
Serial.printf("Bridge %u: priority=%d, load=%d%%\n",
fromNode, pkg.priority, pkg.load);
}
);
// Get notified when bridge state changes (new/updated/lost)
mesh.onBridgeCoordinationChanged(
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode,
TSTRING changeType) {
Serial.printf("Bridge %s: %u (role=%s)\n",
changeType.c_str(), fromNode, pkg.role.c_str());
}
);Fields:
-
priority(uint8_t) - Bridge priority (10=highest, 1=lowest) -
role(TSTRING) - Current role: "primary", "secondary", "standby" -
peerBridges(vector<uint32_t>) - List of known bridge node IDs -
load(uint8_t) - Current load percentage (0-100) -
timestamp(uint32_t) - Coordination timestamp
Use Cases:
- Multiple simultaneous bridges (hot standby)
- Load balancing across bridges
- Geographic distribution
- Automatic traffic shaping
- Conflict resolution
Internal package for per-message delivery confirmation (v2.0.0+). Sent
automatically by the receiving node when an application message carries a
msgId (i.e. the sender passed a delivery callback to sendSingle() or
sendBroadcast()). Applications do not construct this package themselves —
use the delivery callback API instead:
mesh.sendSingle(destId, msg,
[](uint32_t nodeId, bool delivered, uint32_t latencyMs) {
// delivered == true → nodeId confirmed receipt (latencyMs round trip)
// delivered == false → no acknowledgment within the timeout
});Fields:
-
from(uint32_t) - Acknowledging node -
dest(uint32_t) - Original sender the ACK is routed back to -
msgId(uint32_t) - ID of the message being acknowledged
Wire notes:
- Routed as a SINGLE package, so it traverses multiple hops
- Delivery confirmation requires version 2.0 on every participating node.
| Type | Class | Purpose |
|---|---|---|
| 200 | SensorPackage |
Environmental sensor data |
| 202 | StatusPackage |
Device health monitoring |
| 204 | MetricsPackage |
Performance metrics |
| 400 | CommandPackage |
Device control |
| 600 | MeshNodeListPackage |
Node inventory |
| 601 | MeshTopologyPackage |
Network topology |
| 602 | MeshAlertPackage |
Network alerts |
| 603 | MeshBridgePackage |
Protocol bridging |
| 604 | EnhancedStatusPackage |
Mesh-wide status |
| 605 | HealthCheckPackage |
Proactive health monitoring |
| 610 | BridgeStatusPackage |
Bridge health monitoring |
| 611 | BridgeElectionPackage |
Failover election |
| 612 | BridgeTakeoverPackage |
Bridge transition and channel handoff |
| 613 | BridgeCoordinationPackage |
Multi-bridge coordination |
| 614 | NTPTimeSyncPackage |
NTP time distribution |
| 620 | GatewayDataPackage |
Internet-bound data routing |
| 621 | GatewayAckPackage |
Gateway delivery acknowledgment |
| 622 | GatewayHeartbeatPackage |
Gateway health monitoring |
| 630 | MessageAckPackage |
Per-message delivery acknowledgment |
See also:
- Alteriom Overview - Extension architecture and goals
- Basic Examples - Complete working examples
- Bridge Failover Guide - Bridge setup and failover