203 Device Management Lab
203.1 Overview
This hands-on lab provides a comprehensive ESP32 simulation for learning production device management concepts including:
- Device shadows and reported/desired state synchronization
- OTA firmware updates with rollback capability
- Health monitoring and heartbeat mechanisms
- Command execution and acknowledgment
- Degraded mode operation
For the framework overview, see Production Architecture Management.
203.2 How to Use This Lab
- Copy the code from the code block below
- Paste it into the Wokwi editor (replace any existing code)
- Click the green Play button to start the simulation
- Open the Serial Monitor (bottom panel) to observe device management operations
- Experiment by modifying configuration values or adding new commands
203.2.1 Complete Device Management Code
Copy and paste this complete implementation into the Wokwi simulator:
/*
* IoT Device Management Platform Simulation
*
* This comprehensive example demonstrates production-grade device management
* concepts including:
* - Device registration and provisioning
* - Heartbeat and health monitoring
* - Configuration management with versioning
* - Command and control patterns
* - Device shadow/twin state synchronization
*
* For educational purposes - simulates cloud connectivity locally
*/
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
// =============================================================================
// CONFIGURATION CONSTANTS
// =============================================================================
// Device identification
#define DEVICE_TYPE "ESP32_SENSOR"
#define FIRMWARE_VERSION "2.1.0"
#define HARDWARE_VERSION "1.0"
// Timing intervals (milliseconds)
#define HEARTBEAT_INTERVAL 10000 // 10 seconds between heartbeats
#define TELEMETRY_INTERVAL 30000 // 30 seconds between telemetry reports
#define CONFIG_CHECK_INTERVAL 60000 // 60 seconds between config sync
#define HEALTH_CHECK_INTERVAL 5000 // 5 seconds between health checks
#define SHADOW_SYNC_INTERVAL 15000 // 15 seconds between shadow syncs
// Thresholds
#define LOW_BATTERY_THRESHOLD 20 // Percentage
#define CRITICAL_BATTERY_THRESHOLD 10 // Percentage
#define MAX_MISSED_HEARTBEATS 3 // Before declaring offline
#define MEMORY_WARNING_THRESHOLD 80 // Heap usage percentage
// EEPROM addresses for persistent storage
#define EEPROM_SIZE 512
#define EEPROM_DEVICE_ID_ADDR 0
#define EEPROM_CONFIG_VERSION_ADDR 50
#define EEPROM_PROVISIONED_ADDR 100
#define EEPROM_TELEMETRY_INTERVAL_ADDR 104
#define EEPROM_REBOOT_COUNT_ADDR 108
// =============================================================================
// DATA STRUCTURES
// =============================================================================
/**
* Device lifecycle states following AWS IoT / Azure IoT Hub patterns
*/
enum DeviceState {
STATE_UNPROVISIONED, // Factory default, needs registration
STATE_PROVISIONING, // Registration in progress
STATE_ACTIVE, // Normal operation
STATE_DEGRADED, // Operational but with issues
STATE_MAINTENANCE, // OTA update or scheduled maintenance
STATE_OFFLINE, // Lost connectivity
STATE_DECOMMISSIONED // End of life
};
/**
* Health status indicators
*/
enum HealthStatus {
HEALTH_GOOD,
HEALTH_WARNING,
HEALTH_CRITICAL
};
/**
* Command types supported by the device
*/
enum CommandType {
CMD_REBOOT,
CMD_FACTORY_RESET,
CMD_UPDATE_CONFIG,
CMD_SET_TELEMETRY_INTERVAL,
CMD_RUN_DIAGNOSTICS,
CMD_ENTER_MAINTENANCE,
CMD_EXIT_MAINTENANCE,
CMD_BLINK_LED,
CMD_READ_SENSOR,
CMD_UNKNOWN
};
/**
* Device shadow structure (AWS IoT Device Shadow / Azure Device Twin pattern)
* Contains both reported (device -> cloud) and desired (cloud -> device) state
*/
struct DeviceShadow {
// Reported state (device reports these values)
struct {
float temperature;
float humidity;
int batteryLevel;
DeviceState state;
HealthStatus health;
unsigned long uptime;
int freeHeap;
int wifiRssi;
String firmwareVersion;
int rebootCount;
unsigned long lastTelemetryTime;
int configVersion;
} reported;
// Desired state (cloud sets these values)
struct {
int telemetryInterval;
bool ledEnabled;
float tempThresholdHigh;
float tempThresholdLow;
int configVersion;
bool maintenanceMode;
} desired;
// Metadata
unsigned long lastSyncTime;
bool pendingSync;
};
/**
* Device configuration structure
*/
struct DeviceConfig {
int version;
int telemetryIntervalMs;
int heartbeatIntervalMs;
float temperatureOffsetC;
float humidityOffsetPercent;
bool deepSleepEnabled;
int deepSleepDurationSec;
bool alertsEnabled;
float alertTempHigh;
float alertTempLow;
};
/**
* Command structure for remote commands
*/
struct Command {
int id;
CommandType type;
String payload;
unsigned long timestamp;
bool acknowledged;
};
/**
* Telemetry data structure
*/
struct TelemetryData {
float temperature;
float humidity;
int batteryLevel;
int rssi;
unsigned long timestamp;
int sampleCount;
};
// =============================================================================
// GLOBAL VARIABLES
// =============================================================================
// Device identification
String deviceId = "";
String deviceSecret = ""; // In production, use secure element or TPM
// State management
DeviceState currentState = STATE_UNPROVISIONED;
HealthStatus currentHealth = HEALTH_GOOD;
DeviceShadow shadow;
DeviceConfig config;
// Timing trackers
unsigned long lastHeartbeatTime = 0;
unsigned long lastTelemetryTime = 0;
unsigned long lastConfigCheckTime = 0;
unsigned long lastHealthCheckTime = 0;
unsigned long lastShadowSyncTime = 0;
unsigned long bootTime = 0;
// Counters and metrics
int heartbeatsSent = 0;
int telemetrySent = 0;
int commandsReceived = 0;
int commandsExecuted = 0;
int missedHeartbeats = 0;
int configUpdates = 0;
int shadowSyncs = 0;
int rebootCount = 0;
// Command queue (simple implementation - production would use proper queue)
#define MAX_COMMAND_QUEUE 10
Command commandQueue[MAX_COMMAND_QUEUE];
int commandQueueHead = 0;
int commandQueueTail = 0;
// Simulated sensor values
float simulatedTemperature = 22.5;
float simulatedHumidity = 45.0;
int simulatedBattery = 100;
// LED for visual feedback
const int LED_PIN = 2;
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Generate unique device ID based on ESP32 MAC address
*/
String generateDeviceId() {
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[18];
snprintf(macStr, sizeof(macStr), "ESP32_%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(macStr);
}
/**
* Get state name as string for logging
*/
String getStateName(DeviceState state) {
switch (state) {
case STATE_UNPROVISIONED: return "UNPROVISIONED";
case STATE_PROVISIONING: return "PROVISIONING";
case STATE_ACTIVE: return "ACTIVE";
case STATE_DEGRADED: return "DEGRADED";
case STATE_MAINTENANCE: return "MAINTENANCE";
case STATE_OFFLINE: return "OFFLINE";
case STATE_DECOMMISSIONED: return "DECOMMISSIONED";
default: return "UNKNOWN";
}
}
/**
* Get health status as string
*/
String getHealthName(HealthStatus health) {
switch (health) {
case HEALTH_GOOD: return "GOOD";
case HEALTH_WARNING: return "WARNING";
case HEALTH_CRITICAL: return "CRITICAL";
default: return "UNKNOWN";
}
}
/**
* Format uptime as human-readable string
*/
String formatUptime(unsigned long ms) {
unsigned long seconds = ms / 1000;
unsigned long minutes = seconds / 60;
unsigned long hours = minutes / 60;
unsigned long days = hours / 24;
char buf[32];
snprintf(buf, sizeof(buf), "%lud %02lu:%02lu:%02lu",
days, hours % 24, minutes % 60, seconds % 60);
return String(buf);
}
/**
* Log message with timestamp and category
*/
void logMessage(const char* category, const char* message) {
unsigned long uptime = millis() - bootTime;
Serial.printf("[%s] [%s] %s\n", formatUptime(uptime).c_str(), category, message);
}
/**
* Log formatted message
*/
void logMessageF(const char* category, const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
logMessage(category, buffer);
}
// =============================================================================
// PERSISTENT STORAGE FUNCTIONS
// =============================================================================
/**
* Initialize EEPROM for persistent storage
*/
void initStorage() {
EEPROM.begin(EEPROM_SIZE);
logMessage("STORAGE", "EEPROM initialized");
}
/**
* Save device configuration to EEPROM
*/
void saveConfig() {
EEPROM.writeInt(EEPROM_CONFIG_VERSION_ADDR, config.version);
EEPROM.writeInt(EEPROM_TELEMETRY_INTERVAL_ADDR, config.telemetryIntervalMs);
EEPROM.commit();
logMessageF("STORAGE", "Configuration saved (version %d)", config.version);
}
/**
* Load device configuration from EEPROM
*/
void loadConfig() {
config.version = EEPROM.readInt(EEPROM_CONFIG_VERSION_ADDR);
config.telemetryIntervalMs = EEPROM.readInt(EEPROM_TELEMETRY_INTERVAL_ADDR);
// Validate loaded values, use defaults if invalid
if (config.version < 0 || config.version > 10000) {
config.version = 1;
}
if (config.telemetryIntervalMs < 1000 || config.telemetryIntervalMs > 300000) {
config.telemetryIntervalMs = TELEMETRY_INTERVAL;
}
logMessageF("STORAGE", "Configuration loaded (version %d, telemetry %dms)",
config.version, config.telemetryIntervalMs);
}
/**
* Save reboot counter
*/
void saveRebootCount() {
EEPROM.writeInt(EEPROM_REBOOT_COUNT_ADDR, rebootCount);
EEPROM.commit();
}
/**
* Load reboot counter
*/
void loadRebootCount() {
rebootCount = EEPROM.readInt(EEPROM_REBOOT_COUNT_ADDR);
if (rebootCount < 0 || rebootCount > 100000) {
rebootCount = 0;
}
rebootCount++;
saveRebootCount();
logMessageF("STORAGE", "Boot count: %d", rebootCount);
}
// =============================================================================
// DEVICE PROVISIONING
// =============================================================================
/**
* Initialize default configuration values
*/
void initDefaultConfig() {
config.version = 1;
config.telemetryIntervalMs = TELEMETRY_INTERVAL;
config.heartbeatIntervalMs = HEARTBEAT_INTERVAL;
config.temperatureOffsetC = 0.0;
config.humidityOffsetPercent = 0.0;
config.deepSleepEnabled = false;
config.deepSleepDurationSec = 60;
config.alertsEnabled = true;
config.alertTempHigh = 35.0;
config.alertTempLow = 10.0;
}
/**
* Initialize device shadow with default values
*/
void initShadow() {
// Initialize reported state
shadow.reported.temperature = 0.0;
shadow.reported.humidity = 0.0;
shadow.reported.batteryLevel = 100;
shadow.reported.state = STATE_UNPROVISIONED;
shadow.reported.health = HEALTH_GOOD;
shadow.reported.uptime = 0;
shadow.reported.freeHeap = ESP.getFreeHeap();
shadow.reported.wifiRssi = -100;
shadow.reported.firmwareVersion = FIRMWARE_VERSION;
shadow.reported.rebootCount = rebootCount;
shadow.reported.lastTelemetryTime = 0;
shadow.reported.configVersion = config.version;
// Initialize desired state
shadow.desired.telemetryInterval = config.telemetryIntervalMs;
shadow.desired.ledEnabled = true;
shadow.desired.tempThresholdHigh = 35.0;
shadow.desired.tempThresholdLow = 10.0;
shadow.desired.configVersion = config.version;
shadow.desired.maintenanceMode = false;
// Metadata
shadow.lastSyncTime = 0;
shadow.pendingSync = true;
logMessage("SHADOW", "Device shadow initialized");
}
/**
* Simulate device registration with cloud platform
* In production, this would involve:
* - TLS mutual authentication
* - X.509 certificate exchange
* - Device attestation
*/
bool registerDevice() {
logMessage("PROVISION", "Starting device registration...");
currentState = STATE_PROVISIONING;
// Generate device ID
deviceId = generateDeviceId();
logMessageF("PROVISION", "Device ID: %s", deviceId.c_str());
// Simulate cloud registration handshake
logMessage("PROVISION", "Connecting to device registry...");
delay(500); // Simulate network latency
logMessage("PROVISION", "Exchanging authentication tokens...");
delay(300);
logMessage("PROVISION", "Receiving initial configuration...");
delay(200);
// Simulate successful registration
logMessage("PROVISION", "Device registered successfully!");
// Update state
currentState = STATE_ACTIVE;
shadow.reported.state = STATE_ACTIVE;
return true;
}
/**
* Provision device with initial configuration from cloud
*/
void provisionDevice() {
logMessage("PROVISION", "Applying initial provisioning configuration...");
// In production, this would receive configuration from cloud
// For simulation, we use defaults with some variations
config.telemetryIntervalMs = TELEMETRY_INTERVAL;
config.heartbeatIntervalMs = HEARTBEAT_INTERVAL;
config.alertsEnabled = true;
config.alertTempHigh = 30.0;
config.alertTempLow = 15.0;
// Save provisioned state
EEPROM.writeBool(EEPROM_PROVISIONED_ADDR, true);
saveConfig();
logMessage("PROVISION", "Device provisioned successfully");
printCurrentConfig();
}
/**
* Print current configuration
*/
void printCurrentConfig() {
Serial.println("\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β CURRENT DEVICE CONFIGURATION β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β Config Version: %d \n", config.version);
Serial.printf("β Telemetry Interval: %d ms \n", config.telemetryIntervalMs);
Serial.printf("β Heartbeat Interval: %d ms \n", config.heartbeatIntervalMs);
Serial.printf("β Temp Offset: %.2f Β°C \n", config.temperatureOffsetC);
Serial.printf("β Humidity Offset: %.2f %% \n", config.humidityOffsetPercent);
Serial.printf("β Alerts Enabled: %s \n", config.alertsEnabled ? "Yes" : "No");
Serial.printf("β Alert Temp High: %.1f Β°C \n", config.alertTempHigh);
Serial.printf("β Alert Temp Low: %.1f Β°C \n", config.alertTempLow);
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n");
}
// =============================================================================
// HEARTBEAT AND HEALTH MONITORING
// =============================================================================
/**
* Send heartbeat to cloud platform
* Heartbeats indicate device is alive and operational
*/
void sendHeartbeat() {
StaticJsonDocument<256> heartbeat;
heartbeat["deviceId"] = deviceId;
heartbeat["type"] = "heartbeat";
heartbeat["timestamp"] = millis();
heartbeat["state"] = getStateName(currentState);
heartbeat["health"] = getHealthName(currentHealth);
heartbeat["uptime"] = millis() - bootTime;
heartbeat["freeHeap"] = ESP.getFreeHeap();
heartbeat["sequence"] = heartbeatsSent;
// Serialize and "send" (log for simulation)
String output;
serializeJson(heartbeat, output);
logMessageF("HEARTBEAT", "Sent #%d: %s", heartbeatsSent, output.c_str());
heartbeatsSent++;
lastHeartbeatTime = millis();
missedHeartbeats = 0; // Reset missed counter on successful send
}
/**
* Perform comprehensive health check
* Evaluates multiple metrics to determine overall device health
*/
HealthStatus performHealthCheck() {
HealthStatus newHealth = HEALTH_GOOD;
String issues = "";
// Check 1: Memory usage
int freeHeap = ESP.getFreeHeap();
int totalHeap = ESP.getHeapSize();
int heapUsagePercent = 100 - (freeHeap * 100 / totalHeap);
if (heapUsagePercent > MEMORY_WARNING_THRESHOLD) {
newHealth = HEALTH_WARNING;
issues += "High memory usage; ";
}
// Check 2: Battery level (simulated)
if (simulatedBattery < CRITICAL_BATTERY_THRESHOLD) {
newHealth = HEALTH_CRITICAL;
issues += "Critical battery; ";
} else if (simulatedBattery < LOW_BATTERY_THRESHOLD) {
if (newHealth < HEALTH_WARNING) newHealth = HEALTH_WARNING;
issues += "Low battery; ";
}
// Check 3: WiFi signal strength (simulated)
int rssi = -55 + random(-10, 10); // Simulate varying signal
if (rssi < -80) {
if (newHealth < HEALTH_WARNING) newHealth = HEALTH_WARNING;
issues += "Weak WiFi signal; ";
}
// Check 4: Temperature within operating range
if (simulatedTemperature > 50 || simulatedTemperature < 0) {
if (newHealth < HEALTH_WARNING) newHealth = HEALTH_WARNING;
issues += "Temperature out of range; ";
}
// Check 5: Sensor data freshness
unsigned long dataAge = millis() - lastTelemetryTime;
if (lastTelemetryTime > 0 && dataAge > config.telemetryIntervalMs * 3) {
if (newHealth < HEALTH_WARNING) newHealth = HEALTH_WARNING;
issues += "Stale sensor data; ";
}
// Log health check results
if (newHealth != currentHealth) {
logMessageF("HEALTH", "Status changed: %s -> %s",
getHealthName(currentHealth).c_str(),
getHealthName(newHealth).c_str());
if (issues.length() > 0) {
logMessageF("HEALTH", "Issues: %s", issues.c_str());
}
// Update device state based on health
if (newHealth == HEALTH_CRITICAL && currentState == STATE_ACTIVE) {
currentState = STATE_DEGRADED;
shadow.reported.state = STATE_DEGRADED;
} else if (newHealth == HEALTH_GOOD && currentState == STATE_DEGRADED) {
currentState = STATE_ACTIVE;
shadow.reported.state = STATE_ACTIVE;
}
}
currentHealth = newHealth;
shadow.reported.health = newHealth;
lastHealthCheckTime = millis();
return newHealth;
}
/**
* Print detailed health report
*/
void printHealthReport() {
int freeHeap = ESP.getFreeHeap();
int totalHeap = ESP.getHeapSize();
int heapUsagePercent = 100 - (freeHeap * 100 / totalHeap);
Serial.println("\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β DEVICE HEALTH REPORT β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β Overall Health: %s \n", getHealthName(currentHealth).c_str());
Serial.printf("β Device State: %s \n", getStateName(currentState).c_str());
Serial.printf("β Uptime: %s \n", formatUptime(millis() - bootTime).c_str());
Serial.println("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β Free Heap: %d bytes (%d%% used) \n", freeHeap, heapUsagePercent);
Serial.printf("β Battery Level: %d%% \n", simulatedBattery);
Serial.printf("β WiFi RSSI: %d dBm \n", -55 + random(-10, 10));
Serial.printf("β Temperature: %.1f Β°C \n", simulatedTemperature);
Serial.printf("β Humidity: %.1f %% \n", simulatedHumidity);
Serial.println("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β Heartbeats Sent: %d \n", heartbeatsSent);
Serial.printf("β Telemetry Sent: %d \n", telemetrySent);
Serial.printf("β Commands Executed: %d \n", commandsExecuted);
Serial.printf("β Reboot Count: %d \n", rebootCount);
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n");
}
// =============================================================================
// TELEMETRY AND DATA REPORTING
// =============================================================================
/**
* Read sensor data (simulated for this example)
* In production, this would read actual I2C/SPI sensors
*/
TelemetryData readSensors() {
TelemetryData data;
// Simulate realistic sensor variations
simulatedTemperature += random(-10, 10) / 10.0;
simulatedTemperature = constrain(simulatedTemperature, 15.0, 35.0);
simulatedHumidity += random(-20, 20) / 10.0;
simulatedHumidity = constrain(simulatedHumidity, 30.0, 70.0);
// Simulate slow battery drain
if (random(0, 100) < 5) {
simulatedBattery = max(0, simulatedBattery - 1);
}
// Apply calibration offsets from config
data.temperature = simulatedTemperature + config.temperatureOffsetC;
data.humidity = simulatedHumidity + config.humidityOffsetPercent;
data.batteryLevel = simulatedBattery;
data.rssi = -55 + random(-10, 10);
data.timestamp = millis();
data.sampleCount = telemetrySent + 1;
return data;
}
/**
* Send telemetry data to cloud
*/
void sendTelemetry() {
TelemetryData data = readSensors();
// Build telemetry JSON
StaticJsonDocument<512> telemetry;
telemetry["deviceId"] = deviceId;
telemetry["type"] = "telemetry";
telemetry["timestamp"] = data.timestamp;
telemetry["sequence"] = telemetrySent;
JsonObject sensors = telemetry.createNestedObject("sensors");
sensors["temperature"]["value"] = data.temperature;
sensors["temperature"]["unit"] = "celsius";
sensors["humidity"]["value"] = data.humidity;
sensors["humidity"]["unit"] = "percent";
JsonObject device = telemetry.createNestedObject("device");
device["battery"] = data.batteryLevel;
device["rssi"] = data.rssi;
device["uptime"] = millis() - bootTime;
device["freeHeap"] = ESP.getFreeHeap();
// Serialize and "send"
String output;
serializeJson(telemetry, output);
logMessageF("TELEMETRY", "Sent #%d: Temp=%.1fΒ°C, Humidity=%.1f%%, Battery=%d%%",
telemetrySent, data.temperature, data.humidity, data.batteryLevel);
// Update shadow with reported values
shadow.reported.temperature = data.temperature;
shadow.reported.humidity = data.humidity;
shadow.reported.batteryLevel = data.batteryLevel;
shadow.reported.wifiRssi = data.rssi;
shadow.reported.lastTelemetryTime = data.timestamp;
shadow.pendingSync = true;
// Check for threshold alerts
if (config.alertsEnabled) {
if (data.temperature > config.alertTempHigh) {
logMessageF("ALERT", "Temperature ABOVE threshold: %.1fΒ°C > %.1fΒ°C",
data.temperature, config.alertTempHigh);
}
if (data.temperature < config.alertTempLow) {
logMessageF("ALERT", "Temperature BELOW threshold: %.1fΒ°C < %.1fΒ°C",
data.temperature, config.alertTempLow);
}
}
telemetrySent++;
lastTelemetryTime = millis();
}
// =============================================================================
// DEVICE SHADOW / TWIN MANAGEMENT
// =============================================================================
/**
* Synchronize device shadow with cloud
* This implements the AWS IoT Device Shadow / Azure Device Twin pattern
*/
void syncShadow() {
// Update reported state
shadow.reported.uptime = millis() - bootTime;
shadow.reported.freeHeap = ESP.getFreeHeap();
shadow.reported.state = currentState;
shadow.reported.health = currentHealth;
shadow.reported.configVersion = config.version;
// Build shadow document
StaticJsonDocument<1024> shadowDoc;
// Reported section (device -> cloud)
JsonObject reported = shadowDoc.createNestedObject("state").createNestedObject("reported");
reported["temperature"] = shadow.reported.temperature;
reported["humidity"] = shadow.reported.humidity;
reported["batteryLevel"] = shadow.reported.batteryLevel;
reported["state"] = getStateName(shadow.reported.state);
reported["health"] = getHealthName(shadow.reported.health);
reported["uptime"] = shadow.reported.uptime;
reported["freeHeap"] = shadow.reported.freeHeap;
reported["wifiRssi"] = shadow.reported.wifiRssi;
reported["firmwareVersion"] = shadow.reported.firmwareVersion;
reported["rebootCount"] = shadow.reported.rebootCount;
reported["configVersion"] = shadow.reported.configVersion;
// Desired section (cloud -> device) - showing current desired state
JsonObject desired = shadowDoc["state"].createNestedObject("desired");
desired["telemetryInterval"] = shadow.desired.telemetryInterval;
desired["ledEnabled"] = shadow.desired.ledEnabled;
desired["tempThresholdHigh"] = shadow.desired.tempThresholdHigh;
desired["tempThresholdLow"] = shadow.desired.tempThresholdLow;
desired["configVersion"] = shadow.desired.configVersion;
desired["maintenanceMode"] = shadow.desired.maintenanceMode;
// Metadata
shadowDoc["metadata"]["lastSync"] = millis();
shadowDoc["version"] = shadowSyncs + 1;
// Serialize and log
String output;
serializeJsonPretty(shadowDoc, output);
logMessage("SHADOW", "Shadow synchronized:");
Serial.println(output);
shadow.lastSyncTime = millis();
shadow.pendingSync = false;
shadowSyncs++;
lastShadowSyncTime = millis();
}
/**
* Process shadow delta (differences between desired and reported)
* This is called when cloud updates the desired state
*/
void processShadowDelta() {
logMessage("SHADOW", "Processing shadow delta...");
// Check telemetry interval change
if (shadow.desired.telemetryInterval != config.telemetryIntervalMs) {
logMessageF("SHADOW", "Telemetry interval changed: %d -> %d ms",
config.telemetryIntervalMs, shadow.desired.telemetryInterval);
config.telemetryIntervalMs = shadow.desired.telemetryInterval;
saveConfig();
}
// Check LED state change
if (shadow.desired.ledEnabled) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
// Check threshold changes
if (shadow.desired.tempThresholdHigh != config.alertTempHigh) {
config.alertTempHigh = shadow.desired.tempThresholdHigh;
logMessageF("SHADOW", "High temp threshold updated: %.1fΒ°C", config.alertTempHigh);
}
if (shadow.desired.tempThresholdLow != config.alertTempLow) {
config.alertTempLow = shadow.desired.tempThresholdLow;
logMessageF("SHADOW", "Low temp threshold updated: %.1fΒ°C", config.alertTempLow);
}
// Check maintenance mode
if (shadow.desired.maintenanceMode && currentState == STATE_ACTIVE) {
currentState = STATE_MAINTENANCE;
shadow.reported.state = STATE_MAINTENANCE;
logMessage("SHADOW", "Entering maintenance mode");
} else if (!shadow.desired.maintenanceMode && currentState == STATE_MAINTENANCE) {
currentState = STATE_ACTIVE;
shadow.reported.state = STATE_ACTIVE;
logMessage("SHADOW", "Exiting maintenance mode");
}
// Mark for sync to update reported state
shadow.pendingSync = true;
}
// =============================================================================
// COMMAND AND CONTROL
// =============================================================================
/**
* Parse command type from string
*/
CommandType parseCommandType(const String& cmd) {
if (cmd == "reboot") return CMD_REBOOT;
if (cmd == "factory_reset") return CMD_FACTORY_RESET;
if (cmd == "update_config") return CMD_UPDATE_CONFIG;
if (cmd == "set_telemetry_interval") return CMD_SET_TELEMETRY_INTERVAL;
if (cmd == "run_diagnostics") return CMD_RUN_DIAGNOSTICS;
if (cmd == "enter_maintenance") return CMD_ENTER_MAINTENANCE;
if (cmd == "exit_maintenance") return CMD_EXIT_MAINTENANCE;
if (cmd == "blink_led") return CMD_BLINK_LED;
if (cmd == "read_sensor") return CMD_READ_SENSOR;
return CMD_UNKNOWN;
}
/**
* Add command to queue
*/
void queueCommand(int id, const String& type, const String& payload) {
Command cmd;
cmd.id = id;
cmd.type = parseCommandType(type);
cmd.payload = payload;
cmd.timestamp = millis();
cmd.acknowledged = false;
commandQueue[commandQueueTail] = cmd;
commandQueueTail = (commandQueueTail + 1) % MAX_COMMAND_QUEUE;
commandsReceived++;
logMessageF("COMMAND", "Queued command #%d: %s", id, type.c_str());
}
/**
* Execute a single command
*/
void executeCommand(Command& cmd) {
logMessageF("COMMAND", "Executing command #%d: type=%d", cmd.id, cmd.type);
switch (cmd.type) {
case CMD_REBOOT:
logMessage("COMMAND", "Reboot requested - will reboot in 2 seconds");
delay(2000);
ESP.restart();
break;
case CMD_FACTORY_RESET:
logMessage("COMMAND", "Factory reset requested");
config.version = 1;
config.telemetryIntervalMs = TELEMETRY_INTERVAL;
saveConfig();
logMessage("COMMAND", "Configuration reset to factory defaults");
printCurrentConfig();
break;
case CMD_UPDATE_CONFIG:
logMessage("COMMAND", "Configuration update received");
config.version++;
saveConfig();
configUpdates++;
printCurrentConfig();
break;
case CMD_SET_TELEMETRY_INTERVAL:
{
int newInterval = cmd.payload.toInt();
if (newInterval >= 1000 && newInterval <= 300000) {
config.telemetryIntervalMs = newInterval;
shadow.desired.telemetryInterval = newInterval;
saveConfig();
logMessageF("COMMAND", "Telemetry interval set to %d ms", newInterval);
} else {
logMessage("COMMAND", "Invalid telemetry interval (must be 1000-300000 ms)");
}
}
break;
case CMD_RUN_DIAGNOSTICS:
logMessage("COMMAND", "Running diagnostics...");
printHealthReport();
printCurrentConfig();
break;
case CMD_ENTER_MAINTENANCE:
currentState = STATE_MAINTENANCE;
shadow.reported.state = STATE_MAINTENANCE;
shadow.desired.maintenanceMode = true;
logMessage("COMMAND", "Entered maintenance mode");
break;
case CMD_EXIT_MAINTENANCE:
currentState = STATE_ACTIVE;
shadow.reported.state = STATE_ACTIVE;
shadow.desired.maintenanceMode = false;
logMessage("COMMAND", "Exited maintenance mode");
break;
case CMD_BLINK_LED:
logMessage("COMMAND", "Blinking LED...");
for (int i = 0; i < 5; i++) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(200);
}
break;
case CMD_READ_SENSOR:
{
TelemetryData data = readSensors();
logMessageF("COMMAND", "Sensor reading: Temp=%.1fΒ°C, Humidity=%.1f%%",
data.temperature, data.humidity);
}
break;
default:
logMessageF("COMMAND", "Unknown command type: %d", cmd.type);
break;
}
cmd.acknowledged = true;
commandsExecuted++;
shadow.pendingSync = true;
}
/**
* Process all queued commands
*/
void processCommandQueue() {
while (commandQueueHead != commandQueueTail) {
Command& cmd = commandQueue[commandQueueHead];
if (!cmd.acknowledged) {
executeCommand(cmd);
}
commandQueueHead = (commandQueueHead + 1) % MAX_COMMAND_QUEUE;
}
}
/**
* Simulate receiving commands from cloud
* In production, these would come via MQTT subscriptions
*/
void simulateIncomingCommands() {
// Randomly simulate incoming commands for demonstration
static unsigned long lastCommandTime = 0;
static int commandIdCounter = 1;
if (millis() - lastCommandTime > 45000) { // Every 45 seconds
int cmdType = random(0, 5);
switch (cmdType) {
case 0:
queueCommand(commandIdCounter++, "run_diagnostics", "");
break;
case 1:
queueCommand(commandIdCounter++, "read_sensor", "");
break;
case 2:
queueCommand(commandIdCounter++, "blink_led", "");
break;
case 3:
// Simulate config change via shadow
shadow.desired.telemetryInterval = random(2, 6) * 10000;
processShadowDelta();
break;
case 4:
// Simulate threshold change
shadow.desired.tempThresholdHigh = 25.0 + random(0, 15);
processShadowDelta();
break;
}
lastCommandTime = millis();
}
}
// =============================================================================
// CONFIGURATION MANAGEMENT
// =============================================================================
/**
* Check for configuration updates from cloud
* In production, this would poll or subscribe to config topics
*/
void checkConfigUpdates() {
// Simulate occasional config updates
static int configCheckCount = 0;
configCheckCount++;
// Every 5th check, simulate a config update
if (configCheckCount % 5 == 0) {
logMessage("CONFIG", "Checking for configuration updates...");
// Simulate receiving new config from cloud
int cloudConfigVersion = config.version + 1;
if (cloudConfigVersion > config.version) {
logMessageF("CONFIG", "New configuration available: v%d -> v%d",
config.version, cloudConfigVersion);
// Simulate applying new config
config.version = cloudConfigVersion;
// Random config changes for demonstration
if (random(0, 2) == 0) {
config.alertTempHigh = 25.0 + random(0, 10);
logMessageF("CONFIG", "Updated high temp threshold: %.1fΒ°C", config.alertTempHigh);
}
if (random(0, 2) == 0) {
config.alertTempLow = 10.0 + random(0, 10);
logMessageF("CONFIG", "Updated low temp threshold: %.1fΒ°C", config.alertTempLow);
}
saveConfig();
configUpdates++;
shadow.reported.configVersion = config.version;
shadow.pendingSync = true;
printCurrentConfig();
} else {
logMessage("CONFIG", "Configuration is up to date");
}
}
lastConfigCheckTime = millis();
}
// =============================================================================
// MAIN SETUP AND LOOP
// =============================================================================
/**
* Print startup banner
*/
void printBanner() {
Serial.println("\n");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β IoT DEVICE MANAGEMENT PLATFORM SIMULATION β");
Serial.println("β β");
Serial.println("β Demonstrating production device management concepts: β");
Serial.println("β - Device registration and provisioning β");
Serial.println("β - Heartbeat and health monitoring β");
Serial.println("β - Configuration management β");
Serial.println("β - Command and control patterns β");
Serial.println("β - Device shadow/twin concepts β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β Firmware Version: %s \n", FIRMWARE_VERSION);
Serial.printf("β Hardware Version: %s \n", HARDWARE_VERSION);
Serial.printf("β Device Type: %s \n", DEVICE_TYPE);
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n");
}
/**
* Print operational status summary
*/
void printStatusSummary() {
Serial.println("\nββββββββββββββββββ STATUS SUMMARY βββββββββββββββββββ");
Serial.printf(" State: %s | Health: %s | Uptime: %s\n",
getStateName(currentState).c_str(),
getHealthName(currentHealth).c_str(),
formatUptime(millis() - bootTime).c_str());
Serial.printf(" Heartbeats: %d | Telemetry: %d | Commands: %d | Shadow Syncs: %d\n",
heartbeatsSent, telemetrySent, commandsExecuted, shadowSyncs);
Serial.printf(" Config Version: %d | Battery: %d%% | Free Heap: %d bytes\n",
config.version, simulatedBattery, ESP.getFreeHeap());
Serial.println("βββββββββββββββββββββββββββββββββββββββββββββββββββββ\n");
}
/**
* Arduino setup function
*/
void setup() {
// Initialize serial communication
Serial.begin(115200);
while (!Serial) delay(10);
bootTime = millis();
// Initialize LED
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Print startup banner
printBanner();
// Initialize persistent storage
initStorage();
loadRebootCount();
loadConfig();
// Initialize default configuration
initDefaultConfig();
// Initialize device shadow
initShadow();
// Start device registration/provisioning
logMessage("SYSTEM", "Starting device initialization...");
if (registerDevice()) {
provisionDevice();
// Initial heartbeat
sendHeartbeat();
// Initial health check
performHealthCheck();
// Initial shadow sync
syncShadow();
logMessage("SYSTEM", "Device initialization complete!");
Serial.println("\n");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β Device is now ACTIVE and operational β");
Serial.println("β Watch the serial monitor for device management events: β");
Serial.println("β - HEARTBEAT: Regular keep-alive messages β");
Serial.println("β - TELEMETRY: Sensor data reports β");
Serial.println("β - HEALTH: Device health checks β");
Serial.println("β - SHADOW: Device twin synchronization β");
Serial.println("β - COMMAND: Remote command execution β");
Serial.println("β - CONFIG: Configuration updates β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n");
} else {
logMessage("SYSTEM", "ERROR: Device registration failed!");
currentState = STATE_UNPROVISIONED;
}
}
/**
* Arduino main loop
*/
void loop() {
unsigned long currentTime = millis();
// Only run management tasks if device is provisioned
if (currentState == STATE_UNPROVISIONED) {
delay(1000);
return;
}
// Task 1: Send heartbeat at regular intervals
if (currentTime - lastHeartbeatTime >= HEARTBEAT_INTERVAL) {
sendHeartbeat();
}
// Task 2: Send telemetry at configured interval
if (currentTime - lastTelemetryTime >= config.telemetryIntervalMs) {
sendTelemetry();
}
// Task 3: Perform health check
if (currentTime - lastHealthCheckTime >= HEALTH_CHECK_INTERVAL) {
performHealthCheck();
}
// Task 4: Sync device shadow
if (currentTime - lastShadowSyncTime >= SHADOW_SYNC_INTERVAL || shadow.pendingSync) {
syncShadow();
}
// Task 5: Check for configuration updates
if (currentTime - lastConfigCheckTime >= CONFIG_CHECK_INTERVAL) {
checkConfigUpdates();
}
// Task 6: Process command queue
processCommandQueue();
// Task 7: Simulate incoming commands (for demonstration)
simulateIncomingCommands();
// Task 8: Print status summary every minute
static unsigned long lastSummaryTime = 0;
if (currentTime - lastSummaryTime >= 60000) {
printStatusSummary();
lastSummaryTime = currentTime;
}
// Visual heartbeat indicator
static unsigned long lastBlinkTime = 0;
if (currentTime - lastBlinkTime >= 2000) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
lastBlinkTime = currentTime;
}
// Small delay to prevent watchdog issues
delay(10);
}203.2.2 Challenge Exercises
After running the simulation and observing the device management patterns, try these exercises to deepen your understanding:
Task: Change the heartbeat interval from 10 seconds to 5 seconds.
Steps: 1. Find the HEARTBEAT_INTERVAL constant at the top of the code 2. Change 10000 to 5000 3. Observe how this affects the frequency of heartbeat messages
Learning Point: In production systems, heartbeat frequency is a tradeoff between responsiveness (detecting failures quickly) and bandwidth/power consumption.
Task: Add a check for high humidity (above 80%) that triggers a WARNING status.
Steps: 1. Find the performHealthCheck() function 2. Add a new condition after the temperature check:
// Check humidity level
if (simulatedHumidity > 80.0) {
if (newHealth < HEALTH_WARNING) newHealth = HEALTH_WARNING;
issues += "High humidity; ";
}- Run the simulation and observe when the humidity warning triggers
Learning Point: Health checks should cover all environmental and operational parameters that could affect device reliability.
Task: Add a command to set the temperature alert threshold.
Steps: 1. Add CMD_SET_TEMP_THRESHOLD to the CommandType enum 2. Add the case to parseCommandType():
if (cmd == "set_temp_threshold") return CMD_SET_TEMP_THRESHOLD;- Add the execution case to
executeCommand():
case CMD_SET_TEMP_THRESHOLD:
{
float newThreshold = cmd.payload.toFloat();
if (newThreshold > 0 && newThreshold < 50) {
config.alertTempHigh = newThreshold;
logMessageF("COMMAND", "Temperature threshold set to %.1fΒ°C", newThreshold);
}
}
break;Learning Point: Command frameworks should be extensible to support new device capabilities without major refactoring.
- Production Architecture Management - Framework overview, architecture components
- Production Case Studies - Worked examples and deployment pitfalls
- Device Management Lab (this page) - Hands-on ESP32 lab
- Production Resources - Quiz, summaries, visual galleries
203.3 Whatβs Next?
Continue to Production Resources for the comprehensive review quiz, chapter summary, and visual reference gallery.