alteriom overview - Alteriom/painlessMesh GitHub Wiki

Alteriom Extensions Overview

The Alteriom extensions provide production-ready, type-safe packages for common IoT scenarios. These extensions demonstrate best practices for the painlessMesh plugin system while providing immediately useful functionality for sensor networks, device control, and system monitoring.

What are Alteriom Extensions?

Alteriom extensions are pre-built painlessMesh packages that handle common IoT communication patterns:

  • Environmental Monitoring: Temperature, humidity, pressure sensors
  • Device Control: Commands for actuators, displays, relays
  • Health Monitoring: Device status, diagnostics, and telemetry
  • Type Safety: Compile-time validation and automatic serialization
  • Production Ready: Tested, documented, and optimized implementations

Package Types

SensorPackage (Type 200)

For broadcasting environmental sensor data across the mesh.

alteriom::SensorPackage sensor;
sensor.temperature = 23.5;
sensor.humidity = 65.0;
sensor.pressure = 1013.25;
sensor.sensorId = 1001;
sensor.timestamp = mesh.getNodeTime();
sensor.batteryLevel = 85;

mesh.sendPackage(&sensor);

Use Cases:

  • Weather stations
  • Environmental monitoring
  • HVAC system feedback
  • Greenhouse automation
  • Industrial sensor networks

CommandPackage (Type 400)

For sending control commands to specific devices.

alteriom::CommandPackage cmd;
cmd.dest = targetNodeId;
cmd.command = 1; // LED_CONTROL
cmd.targetDevice = 100; // LED strip ID
cmd.parameters = "{\"brightness\":75,\"color\":\"blue\"}";
cmd.commandId = generateCommandId();

mesh.sendPackage(&cmd);

Use Cases:

  • Remote device control
  • Actuator management
  • Display updates
  • System configuration
  • Automation triggers

StatusPackage (Type 202)

For broadcasting device health and operational status.

alteriom::StatusPackage status;
status.deviceStatus = 1; // OPERATIONAL
status.uptime = millis() / 1000;
status.freeMemory = ESP.getFreeHeap();
status.wifiStrength = WiFi.RSSI();
status.firmwareVersion = "2.0.0";

mesh.sendPackage(&status);

Use Cases:

  • System monitoring
  • Predictive maintenance
  • Network diagnostics
  • Performance tracking
  • Remote troubleshooting

Key Features

Type Safety

Compile-time validation prevents common messaging errors:

// Compile error if field types don't match
sensor.temperature = "invalid"; // โŒ Compiler error
sensor.temperature = 25.0;      // โœ… Correct

// IDE autocomplete for all fields
sensor.| // IDE shows: temperature, humidity, pressure, etc.

Automatic Serialization

No manual JSON handling required:

// Automatic serialization to JSON
mesh.sendPackage(&sensor);

// Automatic deserialization from JSON
mesh.onPackage(200, [](protocol::Variant& variant) {
    alteriom::SensorPackage received = variant.to<alteriom::SensorPackage>();
    // All fields automatically populated
});

Cross-Platform Compatibility

Works on ESP32, ESP8266, and desktop platforms:

// TSTRING adapts to platform
#ifdef ESP32
    // Uses Arduino String class
#else
    // Uses std::string on desktop
#endif

Memory Optimization

Efficient memory usage for resource-constrained devices:

// Accurate buffer sizing
size_t bufferSize = sensor.jsonObjectSize();

// Minimal memory footprint
// No unnecessary string copies or allocations

Architecture Integration

Plugin System

Alteriom packages integrate seamlessly with painlessMesh's plugin architecture:

Application Layer (Your Code)
    โ†“
Alteriom Packages (SensorPackage, CommandPackage, StatusPackage)  
    โ†“
painlessMesh Plugin System (SinglePackage, BroadcastPackage)
    โ†“
painlessMesh Core (Mesh, Protocol, Network)

Message Flow

Sensor Reading โ†’ SensorPackage โ†’ JSON โ†’ Mesh Network โ†’ JSON โ†’ SensorPackage โ†’ Handler

Type ID Allocation

The numbers are defined by the package constructors in examples/alteriom/alteriom_sensor_package.hpp; the full list is in Complete Package Type Reference below.

enum AlteriomTypes {
    ALTERIOM_SENSOR = 200,    // SensorPackage
    ALTERIOM_STATUS = 202,    // StatusPackage
    ALTERIOM_METRICS = 204,   // MetricsPackage
    ALTERIOM_COMMAND = 400,   // CommandPackage
};

Getting Started

1. Include Alteriom Headers

#include "painlessMesh.h"
#include "examples/alteriom/alteriom_sensor_package.hpp"

using namespace alteriom; // For convenience

2. Register Package Handlers

void setup() {
    mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
    
    // Register handlers for Alteriom packages
    mesh.onPackage(ALTERIOM_SENSOR, handleSensorData);
    mesh.onPackage(ALTERIOM_COMMAND, handleCommand);
    mesh.onPackage(ALTERIOM_STATUS, handleStatus);
}

3. Implement Handlers

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 (store, analyze, forward, etc.)
    if (sensor.temperature > 30.0) {
        triggerCooling();
    }
}

void handleCommand(protocol::Variant& variant) {
    CommandPackage cmd = variant.to<CommandPackage>();
    
    if (cmd.dest != mesh.getNodeId()) {
        return; // Not for this node
    }
    
    Serial.printf("Command %u for device %u: %u\n",
                 cmd.commandId, cmd.targetDevice, cmd.command);
    
    // Execute command
    executeDeviceCommand(cmd);
    
    // Send acknowledgment
    sendCommandAcknowledgment(cmd);
}

4. Send Packages

// Send sensor data every 30 seconds
Task taskSensorData(30000, TASK_FOREVER, [](){
    SensorPackage sensor;
    sensor.from = mesh.getNodeId();
    sensor.temperature = readTemperature();
    sensor.humidity = readHumidity();
    sensor.pressure = readPressure();
    sensor.sensorId = SENSOR_ID;
    sensor.timestamp = mesh.getNodeTime();
    sensor.batteryLevel = readBatteryLevel();
    
    mesh.sendPackage(&sensor);
});

Advanced Usage

Custom Command Types

Define application-specific command types:

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;
        // ... other commands
    }
}

Command Parameters

Use JSON 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);
    }
}

Status Monitoring

Implement comprehensive device monitoring:

void sendStatusUpdate() {
    StatusPackage status;
    status.from = mesh.getNodeId();
    status.deviceStatus = getDeviceStatus();
    status.uptime = millis() / 1000;
    status.freeMemory = ESP.getFreeHeap();
    status.wifiStrength = WiFi.RSSI();
    status.firmwareVersion = FIRMWARE_VERSION;
    
    // Add custom diagnostics
    if (ESP.getFreeHeap() < 10000) {
        status.deviceStatus |= STATUS_LOW_MEMORY;
    }
    if (WiFi.RSSI() < -80) {
        status.deviceStatus |= STATUS_WEAK_SIGNAL;
    }
    
    mesh.sendPackage(&status);
}

Error Handling

Implement robust error handling:

void handleSensorData(protocol::Variant& variant) {
    try {
        SensorPackage sensor = variant.to<SensorPackage>();
        
        // Validate sensor data
        if (!isValidSensorReading(sensor)) {
            Serial.printf("Invalid sensor data from node %u\n", sensor.from);
            return;
        }
        
        // Check data age
        uint32_t age = mesh.getNodeTime() - sensor.timestamp;
        if (age > MAX_DATA_AGE) {
            Serial.printf("Stale sensor data (age: %u ยตs)\n", age);
            return;
        }
        
        processSensorData(sensor);
        
    } catch (const std::exception& e) {
        Serial.printf("Error processing sensor data: %s\n", e.what());
    }
}

Integration Patterns

Sensor Network Pattern

Central collector with multiple sensor nodes:

class SensorCollector {
private:
    std::map<uint32_t, SensorData> sensorReadings;
    
public:
    void setup() {
        mesh.onPackage(ALTERIOM_SENSOR, [this](protocol::Variant& variant) {
            SensorPackage sensor = variant.to<SensorPackage>();
            storeSensorReading(sensor);
            return false;
        });
    }
    
    void storeSensorReading(const SensorPackage& sensor) {
        sensorReadings[sensor.from] = {
            sensor.temperature,
            sensor.humidity, 
            sensor.pressure,
            sensor.timestamp
        };
        
        // Trigger analysis
        analyzeEnvironmentalData();
    }
};

Command and Control Pattern

Central controller managing multiple devices:

class DeviceController {
private:
    std::map<uint32_t, DeviceInfo> devices;
    
public:
    void controlDevice(uint32_t nodeId, uint32_t deviceId, 
                      uint8_t command, const String& parameters) {
        CommandPackage cmd;
        cmd.dest = nodeId;
        cmd.command = command;
        cmd.targetDevice = deviceId;
        cmd.parameters = parameters;
        cmd.commandId = generateCommandId();
        
        mesh.sendPackage(&cmd);
        
        // Track pending command
        pendingCommands[cmd.commandId] = {nodeId, millis()};
    }
    
    void handleCommandAck(const CommandPackage& ack) {
        auto it = pendingCommands.find(ack.commandId);
        if (it != pendingCommands.end()) {
            Serial.printf("Command %u acknowledged by node %u\n", 
                         ack.commandId, ack.from);
            pendingCommands.erase(it);
        }
    }
};

Health Monitoring Pattern

Network-wide device health monitoring:

class HealthMonitor {
private:
    std::map<uint32_t, DeviceHealth> deviceHealth;
    
public:
    void setup() {
        mesh.onPackage(ALTERIOM_STATUS, [this](protocol::Variant& variant) {
            StatusPackage status = variant.to<StatusPackage>();
            updateDeviceHealth(status);
            return false;
        });
        
        // Check for unhealthy devices every minute
        userScheduler.addTask(Task(60000, TASK_FOREVER, [this]() {
            checkDeviceHealth();
        }));
    }
    
    void updateDeviceHealth(const StatusPackage& status) {
        deviceHealth[status.from] = {
            status.deviceStatus,
            status.uptime,
            status.freeMemory,
            status.wifiStrength,
            mesh.getNodeTime() // Last seen
        };
    }
    
    void checkDeviceHealth() {
        uint32_t now = mesh.getNodeTime();
        
        for (auto& [nodeId, health] : deviceHealth) {
            uint32_t timeSinceLastSeen = now - health.lastSeen;
            
            if (timeSinceLastSeen > DEVICE_TIMEOUT) {
                Serial.printf("Device %u appears offline\n", nodeId);
                triggerAlert(nodeId, "Device offline");
            }
            
            if (health.freeMemory < LOW_MEMORY_THRESHOLD) {
                Serial.printf("Device %u low memory: %u bytes\n", 
                             nodeId, health.freeMemory);
                triggerAlert(nodeId, "Low memory");
            }
        }
    }
};

Best Practices

Performance Optimization

  1. Batch sensor readings when possible
  2. Use appropriate message frequency (don't spam the network)
  3. Implement message filtering to avoid processing irrelevant data
  4. Monitor memory usage especially on ESP8266

Reliability

  1. Validate all received data before processing
  2. Implement timeouts for commands and responses
  3. Handle network partitions gracefully
  4. Add retry logic for critical commands

Security

  1. Validate message sources in handlers
  2. Sanitize command parameters before execution
  3. Implement rate limiting for commands
  4. Consider encryption for sensitive data

Testing

  1. Test with realistic network loads
  2. Simulate node failures and recovery
  3. Validate under memory pressure
  4. Test with maximum expected node count

Bridge Failover Packages

Version 2.0 includes specialized packages for automatic bridge failover and high availability:

BridgeStatusPackage (Type 610)

Monitors bridge health and Internet connectivity status. Bridges periodically broadcast their status to enable failover detection.

Key Fields:

  • internetConnected - Boolean connectivity status
  • routerRSSI - WiFi signal strength in dBm
  • routerChannel - WiFi channel number
  • uptime - Bridge uptime for stability tracking

BridgeElectionPackage (Type 611)

Coordinates automatic bridge failover elections. Nodes broadcast their candidacy with router RSSI for distributed consensus.

Key Fields:

  • routerRSSI - Signal strength for election tiebreaker
  • routerChannel - Router channel used if this candidate wins
  • uptime - Node stability indicator
  • freeMemory - Resource availability
  • routerSSID - Router verification

BridgeTakeoverPackage (Type 612)

Announces bridge role transitions. New bridge broadcasts this package to inform mesh nodes of leadership change.

Key Fields:

  • previousBridge - Previous bridge node ID
  • reason - Human-readable takeover reason
  • routerChannel - Channel peers follow during takeover
  • timestamp - Transition timestamp

BridgeCoordinationPackage (Type 613)

Coordinates multiple simultaneous bridges for load balancing and geographic distribution. Bridges exchange coordination messages for conflict resolution.

Key Fields:

  • priority - Bridge priority (10=highest, 1=lowest)
  • role - Current role: "primary", "secondary", "standby"
  • peerBridges - List of known bridge node IDs
  • load - Current load percentage (0-100)

NTPTimeSyncPackage (Type 614)

Distributes NTP time synchronization from bridge to mesh nodes. Enables mesh-wide time coordination with Internet time sources.

Key Fields:

  • ntpTime - Unix timestamp from NTP server
  • accuracy - Timing precision in milliseconds
  • source - NTP server hostname
  • timestamp - Collection timestamp

Use Cases:

  • Automatic bridge failover for high availability
  • Multi-bridge coordination and load balancing
  • Internet connectivity monitoring
  • Mesh-wide time synchronization
  • Offline operation with RTC backup

See Bridge Failover Guide for implementation details.

Complete Package Type Reference

Type Package Purpose
200 SensorPackage Environmental data
202 StatusPackage Health monitoring
204 MetricsPackage Performance metrics
205 MpptPackage Solar charge controller data (alteriom_custom_package_template.hpp)
400 CommandPackage Device control
600 MeshNodeListPackage Node inventory
601 MeshTopologyPackage Network topology
602 MeshAlertPackage Network alerts
603 MeshBridgePackage Protocol bridging
604 EnhancedStatusPackage Mesh status
605 HealthCheckPackage Health monitoring
610 BridgeStatusPackage Bridge health
611 BridgeElectionPackage Failover election
612 BridgeTakeoverPackage Bridge transition and channel handoff
613 BridgeCoordinationPackage Multi-bridge coordination
614 NTPTimeSyncPackage Time sync

Next Steps

The Alteriom extensions provide a solid foundation for building robust IoT applications with painlessMesh. They demonstrate production-ready patterns while remaining flexible enough to adapt to your specific needs.