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

  1. Copy the code from the code block below
  2. Paste it into the Wokwi editor (replace any existing code)
  3. Click the green Play button to start the simulation
  4. Open the Serial Monitor (bottom panel) to observe device management operations
  5. 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; ";
}
  1. 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;
  1. 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.

NoteChapter Navigation
  1. Production Architecture Management - Framework overview, architecture components
  2. Production Case Studies - Worked examples and deployment pitfalls
  3. Device Management Lab (this page) - Hands-on ESP32 lab
  4. 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.