LAB09: ESP32 Wireless

WiFi, BLE, and MQTT for IoT

PDF Textbook Reference

For detailed theoretical foundations, mathematical proofs, and algorithm derivations, see Chapter 9: ESP32 Wireless Programming for Edge ML in the PDF textbook.

The PDF chapter includes: - Detailed WiFi protocol stack and IEEE 802.11 fundamentals - Complete MQTT architecture and QoS level analysis - In-depth BLE (Bluetooth Low Energy) protocol theory - Mathematical models for power consumption and battery life - Comprehensive wireless security and encryption methods

Open In Colab

Open In Colab

Download Notebook

Learning Objectives

By the end of this lab you will be able to:

  • Configure ESP32 WiFi in station and access-point modes
  • Publish and subscribe to MQTT topics from an ESP32-based node
  • Understand basic BLE communication patterns for short-range sensing
  • Apply simple power-management techniques (sleep modes) for wireless edge nodes

Theory Summary

The ESP32 microcontroller is a game-changer for edge ML applications, combining dual-core processing (240 MHz Xtensa cores), 520 KB SRAM, integrated WiFi and Bluetooth Low Energy, and advanced power management—all in a $3-5 package. Unlike traditional Arduinos that require external wireless modules, the ESP32 enables autonomous edge intelligence with built-in connectivity.

WiFi connectivity operates in two modes: station mode joins existing networks for cloud communication, while access point mode creates local networks for direct device-to-device interaction. For IoT communication, two protocols dominate: HTTP (request/response, stateless, good for infrequent updates) and MQTT (publish/subscribe, persistent connection, ideal for real-time sensor streams). MQTT’s lightweight design reduces overhead from ~200 bytes (HTTP headers) to ~2 bytes per message, crucial for battery-powered deployments.

Bluetooth Low Energy (BLE) complements WiFi for short-range, ultra-low-power scenarios. BLE operates on a peripheral/central model where edge devices advertise services and characteristics that smartphones or gateways can discover and read. Power consumption differs dramatically: WiFi transmission draws 160-260 mA, while BLE beaconing uses only 15-30 mA, and deep sleep mode reduces consumption to 10 μA. For a 2000 mAh battery, this translates from 10 hours of continuous WiFi operation to potentially years with aggressive duty cycling (wake for 5 seconds every 5 minutes to sample and transmit, then deep sleep). The ESP32’s RTC memory preserves critical variables across sleep cycles, enabling stateful battery-powered applications.

Key Concepts at a Glance
  • ESP32 Architecture: Dual-core 240 MHz processor with 520 KB SRAM, 4 MB Flash, integrated WiFi/BLE, and multiple power modes (active, light sleep, deep sleep at 10 μA)
  • WiFi Modes: Station (STA) joins networks for cloud connectivity; Access Point (AP) creates networks for local communication
  • MQTT Protocol: Lightweight publish/subscribe with QoS levels (0=best effort, 1=at-least-once, 2=exactly-once); broker routes messages between clients
  • BLE Services & Characteristics: Services group related data (e.g., sensor readings); characteristics are individual values that can be read, written, or notified
  • Power Management: Deep sleep preserves RTC memory while consuming 10 μA; wake sources include timer, GPIO, and touch sensors
  • OTA Updates: Over-the-air firmware updates enable wireless deployment to remote edge nodes; use dual partitions for safe rollback
  • Battery Life Calculation: Average current = (active_current × active_time + sleep_current × sleep_time) / total_time
Common Pitfalls
  1. WiFi band mismatch: ESP32 only supports 2.4 GHz WiFi, not 5 GHz. Check your router settings if connection fails.

  2. Power supply inadequacy: WiFi transmission draws 240+ mA; USB ports provide only 500 mA. Brownout resets occur with insufficient power. Use dedicated 5V 1A+ supply for stability.

  3. Blocking network calls in main loop: Calling http.POST() or mqtt.connect() without timeout blocks sensor reading and creates unresponsive systems. Always use non-blocking patterns with timeout checks.

  4. MQTT client ID conflicts: Multiple devices with same client ID cause constant disconnections. Generate unique IDs using String clientId = "ESP32_" + String(ESP.getEfuseMac());

  5. Forgetting OTA in firmware updates: If you upload non-OTA firmware via OTA, you permanently lose OTA capability. Always include ArduinoOTA in all builds, even during development.

Quick Reference

Key Formulas

Battery Life with Duty Cycling \[ \text{Average Current} = \frac{I_{\text{active}} \times t_{\text{active}} + I_{\text{sleep}} \times t_{\text{sleep}}}{t_{\text{active}} + t_{\text{sleep}}} \]

\[ \text{Battery Life (hours)} = \frac{\text{Battery Capacity (mAh)}}{\text{Average Current (mA)}} \]

Signal Strength (RSSI) Interpretation \[ \text{Quality} = \begin{cases} \text{Excellent} & \text{RSSI} > -50 \text{ dBm} \\ \text{Good} & -50 > \text{RSSI} > -60 \text{ dBm} \\ \text{Fair} & -60 > \text{RSSI} > -70 \text{ dBm} \\ \text{Poor} & \text{RSSI} < -70 \text{ dBm} \end{cases} \]

Important Parameter Values

Component Parameter Value Notes
WiFi Frequency 2.4 GHz only No 5 GHz support
Modes STA, AP, STA+AP Can run both simultaneously
TX Power 160-260 mA Adjustable with WiFi.setTxPower()
MQTT Default Port 1883 (TCP) 8883 for TLS
Max Packet 256 MB Practical limit ~10 KB
Keep-Alive 15-60 seconds Prevents connection timeout
BLE Frequency 2.4 GHz Coexists with WiFi (time-division)
Range 10-50 meters Depends on environment
TX Power 15-30 mA Much lower than WiFi
Power Active 80-240 mA Varies with WiFi/BLE usage
Light Sleep 0.8 mA WiFi connection maintained
Deep Sleep 10 μA All except RTC powered down
Wake Time ~300 ms From deep sleep to active

Essential Code Patterns

WiFi Connection with Timeout

WiFi.begin(ssid, password);
int timeout = 20;
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
    delay(1000);
    Serial.print(".");
    timeout--;
}
if (WiFi.status() == WL_CONNECTED) {
    Serial.println(WiFi.localIP());
}

MQTT Reconnection Pattern

void reconnectMQTT() {
    while (!mqtt.connected()) {
        String clientId = "ESP32_" + String(random(0xffff), HEX);
        if (mqtt.connect(clientId.c_str())) {
            mqtt.subscribe("edge/commands");
        } else {
            delay(5000);
        }
    }
}

Deep Sleep with RTC Memory

RTC_DATA_ATTR int bootCount = 0;  // Preserved across sleep

void setup() {
    bootCount++;
    // ... do work ...
    esp_sleep_enable_timer_wakeup(60 * 1000000);  // 60 sec
    esp_deep_sleep_start();
}

PDF Cross-References

  • Section 3: WiFi Station and Access Point modes (pages 4-8)
  • Section 4: HTTP communication for edge ML (pages 9-11)
  • Section 5: MQTT protocol and implementation (pages 12-16)
  • Section 6: Bluetooth Low Energy peripheral mode (pages 17-20)
  • Section 7: Power management and deep sleep (pages 21-25)
  • Section 8: Over-the-air firmware updates (pages 26-29)
  • Section 11: Complete troubleshooting guide (pages 35-37)

Try It Yourself: Executable Python Examples

Run these interactive Python examples to understand wireless communication concepts, power management, and network protocol design for edge devices.

Simulated Wireless Sensor Data Transmission

Simulate sensor data transmission over wireless networks with realistic latency and packet loss.

Code
import numpy as np
import matplotlib.pyplot as plt
import time

class WirelessChannel:
    """Simulate wireless channel characteristics."""

    def __init__(self, packet_loss_rate=0.05, latency_ms=50, jitter_ms=20):
        self.packet_loss_rate = packet_loss_rate
        self.latency_ms = latency_ms
        self.jitter_ms = jitter_ms

    def transmit(self, data, timestamp):
        """Simulate packet transmission with loss and latency."""
        # Packet loss
        if np.random.random() < self.packet_loss_rate:
            return None, None  # Packet dropped

        # Variable latency (jitter)
        actual_latency = self.latency_ms + np.random.normal(0, self.jitter_ms)
        actual_latency = max(0, actual_latency)  # No negative latency

        arrival_time = timestamp + actual_latency
        return data, arrival_time

def simulate_sensor_network(duration_sec=30, sample_rate_hz=1, num_sensors=3):
    """Simulate multiple wireless sensor nodes transmitting data."""

    # Different channel conditions for each sensor
    channels = [
        WirelessChannel(packet_loss_rate=0.02, latency_ms=30, jitter_ms=10),  # Good
        WirelessChannel(packet_loss_rate=0.10, latency_ms=80, jitter_ms=30),  # Fair
        WirelessChannel(packet_loss_rate=0.25, latency_ms=150, jitter_ms=50)  # Poor
    ]

    results = {f'sensor_{i}': {'sent': [], 'received': [], 'latencies': [], 'lost': 0}
               for i in range(num_sensors)}

    num_samples = int(duration_sec * sample_rate_hz)

    for t in range(num_samples):
        timestamp_ms = t * (1000 / sample_rate_hz)

        for i in range(num_sensors):
            # Generate sensor reading
            sensor_value = 20 + 5 * np.sin(2 * np.pi * t / 10) + np.random.normal(0, 0.5)

            # Transmit over wireless channel
            received_data, arrival_time = channels[i].transmit(sensor_value, timestamp_ms)

            results[f'sensor_{i}']['sent'].append(timestamp_ms)

            if received_data is not None:
                results[f'sensor_{i}']['received'].append(arrival_time)
                latency = arrival_time - timestamp_ms
                results[f'sensor_{i}']['latencies'].append(latency)
            else:
                results[f'sensor_{i}']['lost'] += 1

    return results

# Run simulation
print("=== Wireless Sensor Network Simulation ===\n")
results = simulate_sensor_network(duration_sec=60, sample_rate_hz=1, num_sensors=3)

# Visualize results
fig, axes = plt.subplots(3, 1, figsize=(12, 10))

sensor_names = ['Sensor 1 (Good)', 'Sensor 2 (Fair)', 'Sensor 3 (Poor)']
colors = ['green', 'orange', 'red']

for i, (sensor_key, color, name) in enumerate(zip(results.keys(), colors, sensor_names)):
    data = results[sensor_key]

    # Plot latencies
    if len(data['latencies']) > 0:
        axes[i].plot(data['received'], data['latencies'], 'o-',
                    color=color, alpha=0.6, linewidth=1, markersize=5)
        axes[i].axhline(np.mean(data['latencies']), color=color,
                       linestyle='--', alpha=0.5, label=f'Mean: {np.mean(data["latencies"]):.1f}ms')

    axes[i].set_xlabel('Time (ms)')
    axes[i].set_ylabel('Latency (ms)')
    axes[i].set_title(f'{name} - Packet Latency')
    axes[i].grid(alpha=0.3)
    axes[i].legend()

    # Statistics
    total_sent = len(data['sent'])
    total_received = len(data['received'])
    packet_loss_pct = (data['lost'] / total_sent) * 100 if total_sent > 0 else 0

    print(f"{name}:")
    print(f"  Packets sent:     {total_sent}")
    print(f"  Packets received: {total_received}")
    print(f"  Packet loss:      {data['lost']} ({packet_loss_pct:.1f}%)")
    if len(data['latencies']) > 0:
        print(f"  Mean latency:     {np.mean(data['latencies']):.1f} ms")
        print(f"  Max latency:      {np.max(data['latencies']):.1f} ms")
        print(f"  Jitter (std dev): {np.std(data['latencies']):.1f} ms")
    print()

plt.tight_layout()
plt.show()
=== Wireless Sensor Network Simulation ===

Sensor 1 (Good):
  Packets sent:     60
  Packets received: 59
  Packet loss:      1 (1.7%)
  Mean latency:     28.2 ms
  Max latency:      51.5 ms
  Jitter (std dev): 9.5 ms

Sensor 2 (Fair):
  Packets sent:     60
  Packets received: 53
  Packet loss:      7 (11.7%)
  Mean latency:     75.9 ms
  Max latency:      131.9 ms
  Jitter (std dev): 32.0 ms

Sensor 3 (Poor):
  Packets sent:     60
  Packets received: 48
  Packet loss:      12 (20.0%)
  Mean latency:     151.5 ms
  Max latency:      302.1 ms
  Jitter (std dev): 43.9 ms

Key Insight: Wireless networks have variable latency (jitter) and packet loss. Edge ML systems must handle missing data gracefully through buffering, retransmission, or state estimation.

Shannon Capacity and Data Rate Calculation

Calculate theoretical maximum data rate for wireless channels using Shannon’s theorem.

Code
import numpy as np
import matplotlib.pyplot as plt

def shannon_capacity_bps(bandwidth_hz, snr_db):
    """
    Calculate Shannon channel capacity in bits per second.

    Shannon-Hartley theorem: C = B × log2(1 + SNR)

    Args:
        bandwidth_hz: Channel bandwidth in Hz
        snr_db: Signal-to-Noise Ratio in dB

    Returns:
        Capacity in bits per second
    """
    snr_linear = 10 ** (snr_db / 10)
    capacity = bandwidth_hz * np.log2(1 + snr_linear)
    return capacity

def db_to_linear(db):
    """Convert dB to linear scale."""
    return 10 ** (db / 10)

def linear_to_db(linear):
    """Convert linear to dB scale."""
    return 10 * np.log10(linear)

# Calculate capacity for different wireless technologies
technologies = [
    ("WiFi 2.4GHz (802.11n)", 20e6, 25),   # 20 MHz bandwidth, 25 dB SNR
    ("Bluetooth Low Energy", 1e6, 15),     # 1 MHz bandwidth, 15 dB SNR
    ("LoRa (SF7)", 125e3, 7),              # 125 kHz bandwidth, 7 dB SNR
    ("Zigbee", 2e6, 20),                   # 2 MHz bandwidth, 20 dB SNR
    ("4G LTE", 20e6, 30),                  # 20 MHz bandwidth, 30 dB SNR
]

print("=== Shannon Capacity for Wireless Technologies ===\n")

for name, bw, snr in technologies:
    capacity_bps = shannon_capacity_bps(bw, snr)
    capacity_mbps = capacity_bps / 1e6

    print(f"{name:30s}")
    print(f"  Bandwidth: {bw/1e6:.2f} MHz")
    print(f"  SNR: {snr} dB (linear: {db_to_linear(snr):.1f})")
    print(f"  Max capacity: {capacity_mbps:.2f} Mbps ({capacity_bps/1e3:.1f} kbps)")
    print()

# Visualize: SNR vs Capacity for fixed bandwidth
snr_range_db = np.linspace(-10, 40, 100)
bandwidths = [125e3, 1e6, 20e6]  # LoRa, BLE, WiFi

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Capacity vs SNR
for bw in bandwidths:
    capacities = [shannon_capacity_bps(bw, snr) / 1e6 for snr in snr_range_db]
    ax1.plot(snr_range_db, capacities, linewidth=2,
            label=f'{bw/1e6:.2f} MHz bandwidth')

ax1.set_xlabel('SNR (dB)')
ax1.set_ylabel('Capacity (Mbps)')
ax1.set_title('Shannon Capacity vs SNR')
ax1.legend()
ax1.grid(alpha=0.3)
ax1.set_xlim(-10, 40)

# Plot 2: Capacity vs Bandwidth
bandwidth_range = np.logspace(3, 8, 100)  # 1 kHz to 100 MHz
snr_values = [0, 10, 20, 30]

for snr in snr_values:
    capacities = [shannon_capacity_bps(bw, snr) / 1e6 for bw in bandwidth_range]
    ax2.plot(bandwidth_range / 1e6, capacities, linewidth=2,
            label=f'SNR = {snr} dB')

ax2.set_xlabel('Bandwidth (MHz)')
ax2.set_ylabel('Capacity (Mbps)')
ax2.set_title('Shannon Capacity vs Bandwidth')
ax2.set_xscale('log')
ax2.legend()
ax2.grid(alpha=0.3, which='both')

plt.tight_layout()
plt.show()

# Practical example: IoT sensor data requirements
print("\n=== IoT Sensor Data Rate Requirements ===\n")

sensors = [
    ("Temperature (JSON)", 50, 1),          # 50 bytes, 1 Hz
    ("Accelerometer (raw)", 12, 50),        # 12 bytes, 50 Hz
    ("Audio (16kHz, 16-bit)", 32000, 1),    # 32 kB/s
    ("Camera (VGA, JPEG)", 10000, 10),      # 10 kB per frame, 10 fps
]

for name, bytes_per_sample, rate_hz in sensors:
    data_rate_bps = bytes_per_sample * 8 * rate_hz
    data_rate_kbps = data_rate_bps / 1000

    print(f"{name:30s}: {data_rate_kbps:8.1f} kbps")

    # Check feasibility for BLE (1 Mbps theoretical)
    ble_capacity = shannon_capacity_bps(1e6, 15) / 1e3  # kbps
    feasible = data_rate_kbps < ble_capacity * 0.5  # Use 50% margin

    print(f"  BLE feasible: {'Yes' if feasible else 'No'} (BLE capacity: {ble_capacity:.0f} kbps)")
    print()
=== Shannon Capacity for Wireless Technologies ===

WiFi 2.4GHz (802.11n)         
  Bandwidth: 20.00 MHz
  SNR: 25 dB (linear: 316.2)
  Max capacity: 166.19 Mbps (166187.5 kbps)

Bluetooth Low Energy          
  Bandwidth: 1.00 MHz
  SNR: 15 dB (linear: 31.6)
  Max capacity: 5.03 Mbps (5027.8 kbps)

LoRa (SF7)                    
  Bandwidth: 0.12 MHz
  SNR: 7 dB (linear: 5.0)
  Max capacity: 0.32 Mbps (323.5 kbps)

Zigbee                        
  Bandwidth: 2.00 MHz
  SNR: 20 dB (linear: 100.0)
  Max capacity: 13.32 Mbps (13316.4 kbps)

4G LTE                        
  Bandwidth: 20.00 MHz
  SNR: 30 dB (linear: 1000.0)
  Max capacity: 199.34 Mbps (199344.5 kbps)


=== IoT Sensor Data Rate Requirements ===

Temperature (JSON)            :      0.4 kbps
  BLE feasible: Yes (BLE capacity: 5028 kbps)

Accelerometer (raw)           :      4.8 kbps
  BLE feasible: Yes (BLE capacity: 5028 kbps)

Audio (16kHz, 16-bit)         :    256.0 kbps
  BLE feasible: Yes (BLE capacity: 5028 kbps)

Camera (VGA, JPEG)            :    800.0 kbps
  BLE feasible: Yes (BLE capacity: 5028 kbps)

Key Insight: Shannon’s theorem sets a fundamental limit on data rate. WiFi has high capacity (100+ Mbps) but high power. BLE has lower capacity (~200 kbps) but 10× lower power. Choose based on your data rate needs.

Power Consumption and Battery Life Estimation

Calculate battery life for different wireless communication patterns and duty cycles.

Code
import numpy as np
import matplotlib.pyplot as plt

class PowerProfile:
    """Power consumption profile for ESP32."""

    # Current draw in different states (mA)
    DEEP_SLEEP = 0.01
    LIGHT_SLEEP = 0.8
    CPU_ACTIVE = 30
    WIFI_CONNECTED = 80
    WIFI_TX = 160
    BLE_ADVERTISING = 20
    BLE_CONNECTED = 40

def calculate_average_current(activities, durations_sec):
    """
    Calculate average current for a duty cycle.

    Args:
        activities: List of (current_mA, name) tuples
        durations_sec: List of durations in seconds

    Returns:
        Average current in mA
    """
    total_time = sum(durations_sec)
    energy_sum = sum(current * duration for (current, _), duration
                     in zip(activities, durations_sec))
    avg_current = energy_sum / total_time
    return avg_current

def battery_life_hours(battery_capacity_mah, avg_current_ma):
    """Calculate battery life in hours."""
    return battery_capacity_mah / avg_current_ma

# Define common use cases
use_cases = {
    "Always-on WiFi": {
        "activities": [
            (PowerProfile.WIFI_CONNECTED, "WiFi idle"),
        ],
        "durations": [1.0]  # Continuous
    },

    "Deep sleep + periodic WiFi (5 min)": {
        "activities": [
            (PowerProfile.WIFI_TX, "WiFi transmit"),
            (PowerProfile.DEEP_SLEEP, "Deep sleep")
        ],
        "durations": [10, 290]  # 10s active, 290s sleep (5 min cycle)
    },

    "BLE continuous advertising": {
        "activities": [
            (PowerProfile.BLE_ADVERTISING, "BLE advertising"),
        ],
        "durations": [1.0]
    },

    "Deep sleep + BLE beacon (1 min)": {
        "activities": [
            (PowerProfile.BLE_ADVERTISING, "BLE advertising"),
            (PowerProfile.DEEP_SLEEP, "Deep sleep")
        ],
        "durations": [5, 55]  # 5s advertising, 55s sleep
    },

    "Aggressive duty cycle (15 min)": {
        "activities": [
            (PowerProfile.CPU_ACTIVE, "Wake up"),
            (PowerProfile.WIFI_TX, "WiFi transmit"),
            (PowerProfile.DEEP_SLEEP, "Deep sleep")
        ],
        "durations": [2, 8, 890]  # 2s CPU, 8s WiFi, 890s sleep (15 min)
    }
}

# Battery capacities for common batteries
batteries = {
    "CR2032 coin cell": 225,
    "18650 Li-ion": 2500,
    "AAA alkaline": 1000,
    "USB power bank": 10000
}

# Calculate and display results
print("=== ESP32 Battery Life Estimation ===\n")

results = {}

for use_case_name, config in use_cases.items():
    avg_current = calculate_average_current(config['activities'], config['durations'])
    results[use_case_name] = avg_current

    print(f"{use_case_name}:")
    print(f"  Average current: {avg_current:.2f} mA")

    for battery_name, capacity_mah in batteries.items():
        life_hours = battery_life_hours(capacity_mah, avg_current)
        life_days = life_hours / 24

        if life_hours < 1:
            life_str = f"{life_hours * 60:.0f} minutes"
        elif life_days < 1:
            life_str = f"{life_hours:.1f} hours"
        else:
            life_str = f"{life_days:.1f} days"

        print(f"    {battery_name:20s}: {life_str}")
    print()

# Visualize battery life comparison
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

# Plot 1: Average current by use case
use_case_names = list(results.keys())
currents = [results[name] for name in use_case_names]

ax1.barh(use_case_names, currents, color='steelblue')
ax1.set_xlabel('Average Current (mA)')
ax1.set_title('Power Consumption by Use Case')
ax1.grid(axis='x', alpha=0.3)

# Plot 2: Battery life for 2500 mAh battery
battery_capacity = 2500  # 18650 Li-ion
life_days = [battery_life_hours(battery_capacity, curr) / 24 for curr in currents]

colors = ['red' if days < 1 else 'orange' if days < 7 else 'green' for days in life_days]

ax2.barh(use_case_names, life_days, color=colors)
ax2.set_xlabel('Battery Life (days)')
ax2.set_title(f'Battery Life with {battery_capacity} mAh Battery')
ax2.set_xscale('log')
ax2.grid(axis='x', alpha=0.3, which='both')

plt.tight_layout()
plt.show()

# Optimization recommendations
print("\n=== Power Optimization Strategies ===\n")

optimizations = [
    ("Use deep sleep between readings", "100x power reduction (80 mA → 0.01 mA)"),
    ("Increase sleep interval (1 min → 5 min)", "5x battery life improvement"),
    ("Use BLE instead of WiFi for low-rate data", "4-8x power reduction"),
    ("Reduce WiFi TX power (WiFi.setTxPower)", "10-20% power reduction"),
    ("Buffer data, batch transmissions", "Minimize wake-up frequency"),
    ("Use light sleep for fast wake-up needs", "Maintains connection but 100x savings vs active"),
]

for strategy, benefit in optimizations:
    print(f"- {strategy}")
    print(f"  Benefit: {benefit}\n")
=== ESP32 Battery Life Estimation ===

Always-on WiFi:
  Average current: 80.00 mA
    CR2032 coin cell    : 2.8 hours
    18650 Li-ion        : 1.3 days
    AAA alkaline        : 12.5 hours
    USB power bank      : 5.2 days

Deep sleep + periodic WiFi (5 min):
  Average current: 5.34 mA
    CR2032 coin cell    : 1.8 days
    18650 Li-ion        : 19.5 days
    AAA alkaline        : 7.8 days
    USB power bank      : 78.0 days

BLE continuous advertising:
  Average current: 20.00 mA
    CR2032 coin cell    : 11.2 hours
    18650 Li-ion        : 5.2 days
    AAA alkaline        : 2.1 days
    USB power bank      : 20.8 days

Deep sleep + BLE beacon (1 min):
  Average current: 1.68 mA
    CR2032 coin cell    : 5.6 days
    18650 Li-ion        : 62.2 days
    AAA alkaline        : 24.9 days
    USB power bank      : 248.6 days

Aggressive duty cycle (15 min):
  Average current: 1.50 mA
    CR2032 coin cell    : 6.3 days
    18650 Li-ion        : 69.5 days
    AAA alkaline        : 27.8 days
    USB power bank      : 278.0 days


=== Power Optimization Strategies ===

- Use deep sleep between readings
  Benefit: 100x power reduction (80 mA → 0.01 mA)

- Increase sleep interval (1 min → 5 min)
  Benefit: 5x battery life improvement

- Use BLE instead of WiFi for low-rate data
  Benefit: 4-8x power reduction

- Reduce WiFi TX power (WiFi.setTxPower)
  Benefit: 10-20% power reduction

- Buffer data, batch transmissions
  Benefit: Minimize wake-up frequency

- Use light sleep for fast wake-up needs
  Benefit: Maintains connection but 100x savings vs active

Key Insight: Deep sleep is critical for battery-powered edge devices. A 5-minute wake interval with 10-second active time provides 30× power savings compared to always-on WiFi, extending battery life from days to months.

MQTT Message Size and Overhead Analysis

Analyze message overhead for different IoT protocols and optimize payload sizes.

Code
import json
import matplotlib.pyplot as plt

def calculate_mqtt_overhead(payload_bytes, topic_length=20, qos=0):
    """
    Calculate MQTT packet overhead.

    MQTT packet structure:
    - Fixed header: 2-5 bytes
    - Variable header: 2-10 bytes (includes message ID for QoS 1/2)
    - Topic length: 2 + topic_length bytes
    - Payload: payload_bytes

    Args:
        payload_bytes: Size of actual data
        topic_length: Length of topic string
        qos: Quality of Service level (0, 1, or 2)

    Returns:
        Total packet size in bytes
    """
    fixed_header = 2
    variable_header = 2 if qos == 0 else 4
    topic_overhead = 2 + topic_length
    total = fixed_header + variable_header + topic_overhead + payload_bytes
    return total

def calculate_http_overhead(payload_bytes, method="POST", endpoint="/api/data"):
    """
    Calculate HTTP overhead (simplified).

    Typical HTTP POST request:
    - Request line: ~30 bytes
    - Headers: ~200 bytes (Host, Content-Type, Content-Length, etc.)
    - Payload: payload_bytes

    Args:
        payload_bytes: Size of JSON payload
        method: HTTP method
        endpoint: API endpoint path

    Returns:
        Total packet size in bytes
    """
    request_line = len(f"{method} {endpoint} HTTP/1.1\r\n")
    headers = 200  # Typical header size
    total = request_line + headers + payload_bytes
    return total

# Compare different message formats
sensor_data = {
    "device_id": "ESP32_A1B2C3",
    "timestamp": 1699564800,
    "temperature": 23.5,
    "humidity": 65.2,
    "battery": 3.7
}

# Format 1: JSON (human-readable)
json_payload = json.dumps(sensor_data)
json_bytes = len(json_payload.encode('utf-8'))

# Format 2: Compact JSON (no spaces)
compact_json = json.dumps(sensor_data, separators=(',', ':'))
compact_bytes = len(compact_json.encode('utf-8'))

# Format 3: CSV-like (minimal)
csv_payload = "ESP32_A1B2C3,1699564800,23.5,65.2,3.7"
csv_bytes = len(csv_payload.encode('utf-8'))

# Format 4: Binary (most compact - simulated)
# Assuming: 16-byte device ID, 4-byte timestamp, 3x 4-byte floats = 32 bytes
binary_bytes = 32

print("=== Message Format Comparison ===\n")

formats = [
    ("JSON (formatted)", json_bytes, json_payload),
    ("JSON (compact)", compact_bytes, compact_json),
    ("CSV", csv_bytes, csv_payload),
    ("Binary", binary_bytes, "(binary data)")
]

for name, size, example in formats:
    print(f"{name:20s}: {size:3d} bytes")
    if size < 100:
        print(f"  Example: {example}")
    print()

# Protocol overhead analysis
print("\n=== Protocol Overhead Analysis ===\n")

payload_sizes = [json_bytes, compact_bytes, csv_bytes, binary_bytes]
format_names = ["JSON", "Compact JSON", "CSV", "Binary"]

for name, payload_size in zip(format_names, payload_sizes):
    mqtt_size = calculate_mqtt_overhead(payload_size, topic_length=20, qos=0)
    http_size = calculate_http_overhead(payload_size)

    mqtt_overhead_pct = ((mqtt_size - payload_size) / payload_size) * 100
    http_overhead_pct = ((http_size - payload_size) / payload_size) * 100

    print(f"{name}:")
    print(f"  Payload: {payload_size} bytes")
    print(f"  MQTT total: {mqtt_size} bytes (overhead: {mqtt_overhead_pct:.0f}%)")
    print(f"  HTTP total: {http_size} bytes (overhead: {http_overhead_pct:.0f}%)")
    print()

# Visualize overhead
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

# Plot 1: Total message size comparison
x = np.arange(len(format_names))
width = 0.35

mqtt_sizes = [calculate_mqtt_overhead(size) for size in payload_sizes]
http_sizes = [calculate_http_overhead(size) for size in payload_sizes]

ax1.bar(x - width/2, mqtt_sizes, width, label='MQTT', color='steelblue')
ax1.bar(x + width/2, http_sizes, width, label='HTTP', color='coral')

ax1.set_xlabel('Message Format')
ax1.set_ylabel('Total Size (bytes)')
ax1.set_title('Total Message Size: MQTT vs HTTP')
ax1.set_xticks(x)
ax1.set_xticklabels(format_names, rotation=15, ha='right')
ax1.legend()
ax1.grid(axis='y', alpha=0.3)

# Plot 2: Overhead percentage
mqtt_overhead_pct = [((calculate_mqtt_overhead(size) - size) / size * 100)
                     for size in payload_sizes]
http_overhead_pct = [((calculate_http_overhead(size) - size) / size * 100)
                     for size in payload_sizes]

ax2.bar(x - width/2, mqtt_overhead_pct, width, label='MQTT', color='steelblue')
ax2.bar(x + width/2, http_overhead_pct, width, label='HTTP', color='coral')

ax2.set_xlabel('Message Format')
ax2.set_ylabel('Overhead (%)')
ax2.set_title('Protocol Overhead Percentage')
ax2.set_xticks(x)
ax2.set_xticklabels(format_names, rotation=15, ha='right')
ax2.legend()
ax2.grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

# Transmission time and energy calculation
print("\n=== Transmission Cost Analysis ===\n")

data_rates = {
    "WiFi (11 Mbps)": 11e6,
    "BLE (1 Mbps)": 1e6,
    "LoRa (5 kbps)": 5e3
}

tx_power = {
    "WiFi (11 Mbps)": 160,  # mA
    "BLE (1 Mbps)": 20,
    "LoRa (5 kbps)": 100
}

for tech_name, data_rate_bps in data_rates.items():
    mqtt_size = calculate_mqtt_overhead(csv_bytes)
    tx_time_sec = (mqtt_size * 8) / data_rate_bps
    energy_mah = (tx_power[tech_name] * tx_time_sec) / 3600

    print(f"{tech_name}:")
    print(f"  Message size: {mqtt_size} bytes")
    print(f"  TX time: {tx_time_sec * 1000:.2f} ms")
    print(f"  Energy: {energy_mah:.6f} mAh")
    print()

print("Optimization: Use compact formats (CSV/Binary) and MQTT for battery-powered devices")
=== Message Format Comparison ===

JSON (formatted)    : 109 bytes

JSON (compact)      : 100 bytes

CSV                 :  37 bytes
  Example: ESP32_A1B2C3,1699564800,23.5,65.2,3.7

Binary              :  32 bytes
  Example: (binary data)


=== Protocol Overhead Analysis ===

JSON:
  Payload: 109 bytes
  MQTT total: 135 bytes (overhead: 24%)
  HTTP total: 334 bytes (overhead: 206%)

Compact JSON:
  Payload: 100 bytes
  MQTT total: 126 bytes (overhead: 26%)
  HTTP total: 325 bytes (overhead: 225%)

CSV:
  Payload: 37 bytes
  MQTT total: 63 bytes (overhead: 70%)
  HTTP total: 262 bytes (overhead: 608%)

Binary:
  Payload: 32 bytes
  MQTT total: 58 bytes (overhead: 81%)
  HTTP total: 257 bytes (overhead: 703%)


=== Transmission Cost Analysis ===

WiFi (11 Mbps):
  Message size: 63 bytes
  TX time: 0.05 ms
  Energy: 0.000002 mAh

BLE (1 Mbps):
  Message size: 63 bytes
  TX time: 0.50 ms
  Energy: 0.000003 mAh

LoRa (5 kbps):
  Message size: 63 bytes
  TX time: 100.80 ms
  Energy: 0.002800 mAh

Optimization: Use compact formats (CSV/Binary) and MQTT for battery-powered devices

Key Insight: MQTT has 30-50% overhead vs HTTP’s 300%+ overhead for small messages. Use compact formats (CSV or binary) instead of JSON to reduce payload by 50-70%, critical for low-bandwidth protocols like LoRa.

Self-Assessment Checkpoints

Test your understanding before proceeding to the exercises.

Answer: Total cycle = 5 minutes = 300 seconds. Active time = 10 seconds, sleep time = 290 seconds. Average current = (I_active × t_active + I_sleep × t_sleep) / total_time = (80mA × 10s + 0.01mA × 290s) / 300s = (800 + 2.9) / 300 = 2.68 mA. For a 2500 mAh battery: Battery life = 2500 / 2.68 = 932 hours = 38.8 days. Without deep sleep (constant 80mA), battery life would be only 31 hours. Sleep modes provide 30× improvement!

Answer: WiFi transmission draws 160-260 mA vs BLE at 15-30 mA (roughly 10× difference) because WiFi has higher bandwidth (54+ Mbps vs 1-2 Mbps), longer range (100m vs 10-50m), and more complex protocols. Use WiFi when: you need internet connectivity, high data rates (video, large files), long-range communication, or cloud integration. Use BLE when: battery life is critical, short-range communication is sufficient (phone to device), low data rates work (sensor readings, commands), or you need smartphone integration without internet. For edge ML: BLE for wearables/beacons sending occasional inference results; WiFi for devices needing cloud model updates or data aggregation.

Answer: (1) Client ID conflict: Multiple devices with the same client ID cause constant disconnections as the broker kicks out duplicates. Solution: Generate unique IDs: String clientId = "ESP32_" + String(random(0xffff), HEX); or use MAC address. (2) Keep-alive timeout: Default 15-second keep-alive is too short for devices with long sleep periods. Set longer: mqtt.setKeepAlive(60); (3) WiFi signal weak: RSSI < -70 dBm causes packet loss. Check WiFi.RSSI() and move closer to router or add antenna. (4) Power supply brownouts: WiFi transmission current spikes cause resets. Use 1A+ power supply. (5) Broker restarts: Check broker logs for crashes or memory issues.

Answer: During deep sleep, the ESP32 powers down everything except the RTC (Real-Time Clock) subsystem to minimize power consumption to 10 μA. Normal variables in SRAM are lost because SRAM is powered off. RTC memory remains powered and preserves its contents. Declaring RTC_DATA_ATTR int bootCount = 0; places the variable in RTC memory (8KB available). When the device wakes, execution starts from scratch (like a reset), but RTC variables retain their previous values. This enables stateful battery-powered applications: count wake cycles, track averages, implement adaptive sampling, or maintain connection state. Without RTC_DATA_ATTR, you’d lose all state and start fresh every wake.

Answer: The ESP32’s WiFi radio hardware only supports 2.4 GHz frequency band (802.11 b/g/n), not 5 GHz (802.11 a/ac). This is a hardware limitation, not a software issue. Even if your router broadcasts both bands with the same SSID, the ESP32 will only see the 2.4 GHz network. Solutions: (1) Ensure router’s 2.4 GHz band is enabled (some networks disable it in preference for 5 GHz), (2) Create separate SSIDs for each band to confirm 2.4 GHz exists, (3) Check channel congestion - 2.4 GHz has only 3 non-overlapping channels (1, 6, 11) vs 5 GHz’s 23 channels, so interference may be high. For newer projects needing 5 GHz, consider ESP32-S3 or ESP32-C6 (WiFi 6).

Complete Code Examples

WiFi + MQTT + Sensor Integration

This complete Arduino sketch demonstrates a production-ready ESP32 sensor node that connects to WiFi, publishes sensor data to an MQTT broker, and handles reconnection gracefully.

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>

// WiFi Configuration
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";

// MQTT Configuration
const char* mqtt_server = "broker.hivemq.com";  // Public broker for testing
const int mqtt_port = 1883;
const char* mqtt_topic_pub = "edge/sensors/node01";
const char* mqtt_topic_sub = "edge/commands/node01";

// Sensor Configuration
#define DHTPIN 4          // GPIO4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// Global Objects
WiFiClient espClient;
PubSubClient mqtt(espClient);

// Connection State
unsigned long lastReconnectAttempt = 0;
unsigned long lastPublish = 0;
const long publishInterval = 10000;  // 10 seconds
int reconnectAttempts = 0;
const int maxReconnectAttempts = 5;

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n=== ESP32 Sensor Node Starting ===");

    // Initialize sensor
    dht.begin();

    // Connect to WiFi with retry logic
    connectWiFi();

    // Configure MQTT
    mqtt.setServer(mqtt_server, mqtt_port);
    mqtt.setCallback(mqttCallback);
    mqtt.setKeepAlive(60);  // Prevent timeout

    // Initial MQTT connection
    reconnectMQTT();
}

void loop() {
    // Ensure WiFi is connected
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi disconnected! Reconnecting...");
        connectWiFi();
    }

    // Ensure MQTT is connected
    if (!mqtt.connected()) {
        unsigned long now = millis();
        // Exponential backoff: wait longer between reconnect attempts
        unsigned long backoff = min(30000, 1000 * (1 << reconnectAttempts));
        if (now - lastReconnectAttempt > backoff) {
            lastReconnectAttempt = now;
            if (reconnectMQTT()) {
                reconnectAttempts = 0;  // Reset on success
            } else {
                reconnectAttempts++;
                if (reconnectAttempts >= maxReconnectAttempts) {
                    Serial.println("Max reconnect attempts reached. Restarting...");
                    ESP.restart();
                }
            }
        }
    } else {
        mqtt.loop();  // Process incoming messages
    }

    // Publish sensor data at regular intervals
    unsigned long now = millis();
    if (now - lastPublish > publishInterval) {
        lastPublish = now;
        publishSensorData();
    }
}

void connectWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    Serial.print("Connecting to WiFi");
    int timeout = 20;  // 20 second timeout

    while (WiFi.status() != WL_CONNECTED && timeout > 0) {
        delay(1000);
        Serial.print(".");
        timeout--;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi Connected!");
        Serial.print("IP Address: ");
        Serial.println(WiFi.localIP());
        Serial.print("Signal Strength (RSSI): ");
        Serial.print(WiFi.RSSI());
        Serial.println(" dBm");
    } else {
        Serial.println("\nWiFi Connection Failed!");
        Serial.println("Restarting in 5 seconds...");
        delay(5000);
        ESP.restart();
    }
}

boolean reconnectMQTT() {
    // Generate unique client ID using MAC address
    String clientId = "ESP32_" + String((uint32_t)ESP.getEfuseMac(), HEX);

    Serial.print("Attempting MQTT connection as ");
    Serial.print(clientId);
    Serial.print("...");

    if (mqtt.connect(clientId.c_str())) {
        Serial.println("connected!");

        // Subscribe to command topic
        mqtt.subscribe(mqtt_topic_sub);
        Serial.print("Subscribed to: ");
        Serial.println(mqtt_topic_sub);

        // Publish online status
        StaticJsonDocument<200> doc;
        doc["device"] = clientId;
        doc["status"] = "online";
        doc["ip"] = WiFi.localIP().toString();
        doc["rssi"] = WiFi.RSSI();

        char buffer[200];
        serializeJson(doc, buffer);
        mqtt.publish(mqtt_topic_pub, buffer, true);  // Retained message

        return true;
    } else {
        Serial.print("failed, rc=");
        Serial.print(mqtt.state());
        Serial.println();
        return false;
    }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Message arrived [");
    Serial.print(topic);
    Serial.print("]: ");

    String message;
    for (int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    Serial.println(message);

    // Parse JSON command
    StaticJsonDocument<200> doc;
    DeserializationError error = deserializeJson(doc, message);

    if (error) {
        Serial.print("JSON parse failed: ");
        Serial.println(error.c_str());
        return;
    }

    // Handle commands
    if (doc.containsKey("interval")) {
        int newInterval = doc["interval"];
        Serial.print("Changing publish interval to: ");
        Serial.println(newInterval);
        // Update interval (add validation in production)
    }

    if (doc.containsKey("restart")) {
        Serial.println("Restart command received");
        delay(1000);
        ESP.restart();
    }
}

void publishSensorData() {
    // Read sensor with error checking
    float temperature = dht.readTemperature();
    float humidity = dht.readHumidity();

    if (isnan(temperature) || isnan(humidity)) {
        Serial.println("Failed to read from DHT sensor!");
        return;
    }

    // Create JSON payload
    StaticJsonDocument<300> doc;
    doc["device"] = String((uint32_t)ESP.getEfuseMac(), HEX);
    doc["timestamp"] = millis();
    doc["temperature"] = round(temperature * 10) / 10.0;  // 1 decimal
    doc["humidity"] = round(humidity * 10) / 10.0;
    doc["rssi"] = WiFi.RSSI();
    doc["uptime"] = millis() / 1000;

    char buffer[300];
    serializeJson(doc, buffer);

    // Publish with QoS 1 (at least once delivery)
    if (mqtt.publish(mqtt_topic_pub, buffer)) {
        Serial.print("Published: ");
        Serial.println(buffer);
    } else {
        Serial.println("Publish failed!");
    }
}

Key Features:

  • WiFi retry logic: 20-second timeout with visual feedback
  • Unique MQTT client ID: Uses ESP32 MAC address to prevent conflicts
  • Exponential backoff: Waits longer between failed reconnection attempts
  • Automatic restart: After 5 failed MQTT reconnects
  • JSON messages: Structured data for easy parsing
  • Command handling: Receives configuration changes via MQTT
  • Signal monitoring: Tracks RSSI for connection quality
  • Error checking: Validates sensor readings before publishing

Required Libraries:

PubSubClient by Nick O'Leary
DHT sensor library by Adafruit
ArduinoJson by Benoit Blanchon

Deep Sleep with RTC Memory

This example shows how to preserve data across sleep cycles and optimize power consumption for battery-powered sensor nodes.

#include <WiFi.h>
#include <PubSubClient.h>

// RTC Memory - Preserved across deep sleep
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR float temperatureSum = 0;
RTC_DATA_ATTR int sampleCount = 0;
RTC_DATA_ATTR bool mqttConnected = false;

// Configuration
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
const char* mqtt_server = "broker.hivemq.com";

#define SENSOR_PIN 34        // ADC pin
#define WAKEUP_GPIO 33       // Button for GPIO wake
#define LED_PIN 2            // Status LED

// Sleep configuration
#define SLEEP_DURATION 60    // 60 seconds
#define SAMPLES_BEFORE_SEND 10  // Send after 10 wake cycles

WiFiClient espClient;
PubSubClient mqtt(espClient);

void setup() {
    Serial.begin(115200);
    delay(100);  // Minimal delay

    pinMode(LED_PIN, OUTPUT);
    pinMode(WAKEUP_GPIO, INPUT_PULLUP);

    bootCount++;
    Serial.println("\n=== Wake-up #" + String(bootCount) + " ===");

    // Determine wake-up cause
    printWakeupReason();

    // Flash LED to show activity
    digitalWrite(LED_PIN, HIGH);

    // Read sensor quickly
    float sensorValue = readSensor();
    temperatureSum += sensorValue;
    sampleCount++;

    Serial.print("Sensor reading: ");
    Serial.println(sensorValue);
    Serial.print("Running average: ");
    Serial.println(temperatureSum / sampleCount);

    // Send data every N wake cycles
    if (sampleCount >= SAMPLES_BEFORE_SEND) {
        sendDataToCloud();
        // Reset accumulators
        temperatureSum = 0;
        sampleCount = 0;
    }

    digitalWrite(LED_PIN, LOW);

    // Configure wake-up sources
    configureWakeup();

    // Enter deep sleep
    Serial.println("Entering deep sleep for " + String(SLEEP_DURATION) + " seconds...");
    Serial.flush();  // Wait for serial to finish
    esp_deep_sleep_start();
}

void loop() {
    // Never reached - deep sleep restarts from setup()
}

void printWakeupReason() {
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();

    switch(wakeup_reason) {
        case ESP_SLEEP_WAKEUP_EXT0:
            Serial.println("Wake-up: External GPIO");
            break;
        case ESP_SLEEP_WAKEUP_EXT1:
            Serial.println("Wake-up: External GPIO (multi-pin)");
            break;
        case ESP_SLEEP_WAKEUP_TIMER:
            Serial.println("Wake-up: Timer");
            break;
        case ESP_SLEEP_WAKEUP_TOUCHPAD:
            Serial.println("Wake-up: Touchpad");
            break;
        case ESP_SLEEP_WAKEUP_ULP:
            Serial.println("Wake-up: ULP program");
            break;
        default:
            Serial.println("Wake-up: Power-on reset");
            bootCount = 1;  // First boot
            break;
    }
}

void configureWakeup() {
    // Wake-up source 1: Timer (primary)
    esp_sleep_enable_timer_wakeup(SLEEP_DURATION * 1000000ULL);  // Convert to microseconds
    Serial.println("Timer wake-up configured for " + String(SLEEP_DURATION) + "s");

    // Wake-up source 2: GPIO button (secondary, for manual trigger)
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0);  // Wake on LOW (button press)
    Serial.println("GPIO wake-up enabled on pin 33 (LOW trigger)");

    // Optional: Wake-up source 3: Touch sensor
    // touchAttachInterrupt(T0, callback, threshold);
    // esp_sleep_enable_touchpad_wakeup();
}

float readSensor() {
    // Read analog sensor (e.g., temperature, light)
    // Average multiple readings for accuracy
    const int numSamples = 10;
    float sum = 0;

    for (int i = 0; i < numSamples; i++) {
        sum += analogRead(SENSOR_PIN);
        delay(10);
    }

    float avg = sum / numSamples;

    // Convert to meaningful units (example: voltage)
    float voltage = avg * (3.3 / 4095.0);

    return voltage;
}

void sendDataToCloud() {
    Serial.println("\n--- Sending accumulated data to cloud ---");

    // Calculate average
    float average = temperatureSum / sampleCount;

    // Quick WiFi connection with timeout
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    int timeout = 10;  // 10 second timeout (battery conservation)
    while (WiFi.status() != WL_CONNECTED && timeout > 0) {
        delay(1000);
        Serial.print(".");
        timeout--;
    }

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("\nWiFi failed. Data will be sent next wake.");
        return;
    }

    Serial.println("\nWiFi connected");

    // Connect to MQTT
    mqtt.setServer(mqtt_server, 1883);
    String clientId = "ESP32_" + String((uint32_t)ESP.getEfuseMac(), HEX);

    if (mqtt.connect(clientId.c_str())) {
        Serial.println("MQTT connected");

        // Publish data
        String payload = "{";
        payload += "\"device\":\"" + clientId + "\",";
        payload += "\"bootCount\":" + String(bootCount) + ",";
        payload += "\"samples\":" + String(sampleCount) + ",";
        payload += "\"average\":" + String(average, 2) + ",";
        payload += "\"battery\":" + String(readBatteryVoltage(), 2);
        payload += "}";

        mqtt.publish("edge/sleep-nodes/data", payload.c_str());
        Serial.println("Published: " + payload);

        mqtt.disconnect();
    } else {
        Serial.println("MQTT connection failed");
    }

    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
}

float readBatteryVoltage() {
    // Example: Read battery voltage via voltage divider on GPIO35
    // Assuming 2:1 divider for 3.7V LiPo
    int raw = analogRead(35);
    return (raw / 4095.0) * 3.3 * 2.0;
}

Power Analysis:

For 5-minute wake interval with 10-second active time:

Active (WiFi): 80 mA × 10s = 800 mAs
Sleep: 0.01 mA × 290s = 2.9 mAs
Average: (800 + 2.9) / 300 = 2.68 mA

Battery Life (2500 mAh): 2500 / 2.68 = 932 hours = 38.8 days

Key Features:

  • RTC memory: Preserves variables across sleep cycles
  • Multiple wake sources: Timer (primary) + GPIO button (manual)
  • Data accumulation: Averages readings before WiFi transmission
  • Fast WiFi: 10-second timeout for battery conservation
  • Battery monitoring: Tracks voltage for low-power alerts
  • Wake-up diagnostics: Identifies what triggered wake-up

Exercise: Modify to wake every 5 minutes, but only connect to WiFi every 6th wake (30 minutes). Calculate new battery life.

BLE Beacon with Sensor Data

This example demonstrates BLE advertising with sensor data embedded in the advertisement packet for ultra-low-power proximity sensing.

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLEAdvertising.h>
#include <DHT.h>

// BLE Configuration
#define SERVICE_UUID        "181A"  // Environmental Sensing Service
#define TEMP_CHAR_UUID      "2A6E"  // Temperature Characteristic
#define HUMIDITY_CHAR_UUID  "2A6F"  // Humidity Characteristic

// Sensor Configuration
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// BLE Objects
BLEServer* pServer = NULL;
BLECharacteristic* pTempChar = NULL;
BLECharacteristic* pHumidityChar = NULL;
BLEAdvertising* pAdvertising = NULL;

// State
bool deviceConnected = false;
bool oldDeviceConnected = false;
unsigned long lastUpdate = 0;
const long updateInterval = 5000;  // 5 seconds

// Callbacks for connection events
class ServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        Serial.println("Client connected!");
    }

    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        Serial.println("Client disconnected!");
    }
};

void setup() {
    Serial.begin(115200);
    Serial.println("\n=== BLE Beacon with Sensor Data ===");

    // Initialize sensor
    dht.begin();

    // Create BLE Device
    BLEDevice::init("ESP32_Sensor_Beacon");

    // Create BLE Server
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks());

    // Create BLE Service
    BLEService* pService = pServer->createService(SERVICE_UUID);

    // Create Temperature Characteristic
    pTempChar = pService->createCharacteristic(
        TEMP_CHAR_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY
    );

    // Create Humidity Characteristic
    pHumidityChar = pService->createCharacteristic(
        HUMIDITY_CHAR_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY
    );

    // Add descriptors for notifications
    pTempChar->addDescriptor(new BLE2902());
    pHumidityChar->addDescriptor(new BLE2902());

    // Start the service
    pService->start();

    // Configure advertising
    pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);

    // Set advertising parameters for low power
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);  // 7.5ms interval
    pAdvertising->setMinPreferred(0x12);  // 22.5ms interval

    // Start advertising
    BLEDevice::startAdvertising();
    Serial.println("BLE Beacon advertising started");

    printDeviceInfo();
}

void loop() {
    unsigned long now = millis();

    // Update sensor data periodically
    if (now - lastUpdate > updateInterval) {
        lastUpdate = now;
        updateSensorData();
    }

    // Handle connection state changes
    if (!deviceConnected && oldDeviceConnected) {
        delay(500);  // Give time for BLE stack
        pServer->startAdvertising();  // Restart advertising
        Serial.println("Restarted advertising");
        oldDeviceConnected = deviceConnected;
    }

    if (deviceConnected && !oldDeviceConnected) {
        oldDeviceConnected = deviceConnected;
    }

    delay(100);  // Small delay to prevent watchdog
}

void updateSensorData() {
    // Read sensor
    float temperature = dht.readTemperature();
    float humidity = dht.readHumidity();

    if (isnan(temperature) || isnan(humidity)) {
        Serial.println("Sensor read failed!");
        return;
    }

    Serial.print("Temp: ");
    Serial.print(temperature);
    Serial.print("°C, Humidity: ");
    Serial.print(humidity);
    Serial.println("%");

    // Update characteristics (BLE uses int16 in 0.01 unit resolution)
    int16_t tempInt = (int16_t)(temperature * 100);
    int16_t humidityInt = (int16_t)(humidity * 100);

    pTempChar->setValue(tempInt);
    pHumidityChar->setValue(humidityInt);

    // Notify connected clients
    if (deviceConnected) {
        pTempChar->notify();
        pHumidityChar->notify();
        Serial.println("Notified connected clients");
    }

    // Update advertising data with latest sensor values
    updateAdvertisingData(temperature, humidity);
}

void updateAdvertisingData(float temp, float humidity) {
    // Create manufacturer-specific data packet
    // Format: Company ID (2 bytes) + Temp (2 bytes) + Humidity (2 bytes) + Battery (1 byte)

    uint8_t advData[7];
    advData[0] = 0xFF;  // Manufacturer Specific Data type
    advData[1] = 0xE5;  // Company ID (example: 0x02E5 for Espressif)
    advData[2] = 0x02;

    // Temperature (signed int, 0.1°C resolution)
    int16_t tempInt = (int16_t)(temp * 10);
    advData[3] = tempInt & 0xFF;
    advData[4] = (tempInt >> 8) & 0xFF;

    // Humidity (unsigned int, 0.1% resolution)
    uint16_t humInt = (uint16_t)(humidity * 10);
    advData[5] = humInt & 0xFF;

    // Battery level (0-100%)
    advData[6] = 100;  // Example: Full battery

    // Update advertising data
    BLEAdvertisementData scanResponse;
    scanResponse.setManufacturerData(std::string((char*)advData, 7));
    pAdvertising->setScanResponseData(scanResponse);

    Serial.println("Updated advertising packet");
}

void printDeviceInfo() {
    Serial.println("\n--- Device Information ---");
    Serial.print("Device Name: ");
    Serial.println(BLEDevice::toString().c_str());
    Serial.print("BLE Address: ");
    Serial.println(BLEDevice::getAddress().toString().c_str());
    Serial.println("\nService UUID: " + String(SERVICE_UUID));
    Serial.println("Temperature UUID: " + String(TEMP_CHAR_UUID));
    Serial.println("Humidity UUID: " + String(HUMIDITY_CHAR_UUID));
    Serial.println("\nUse a BLE scanner app to connect:");
    Serial.println("- nRF Connect (Android/iOS)");
    Serial.println("- LightBlue (iOS)");
    Serial.println("- BLE Scanner (Android)");
    Serial.println("-------------------------\n");
}

Power-Efficient BLE Scanner (Companion Code):

#include <BLEDevice.h>
#include <BLEScan.h>

BLEScan* pBLEScan;

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
        // Filter for our sensor beacons
        if (advertisedDevice.haveServiceUUID() &&
            advertisedDevice.getServiceUUID().toString() == "181a") {

            Serial.println("\n--- Found Sensor Beacon ---");
            Serial.print("Address: ");
            Serial.println(advertisedDevice.getAddress().toString().c_str());
            Serial.print("RSSI: ");
            Serial.println(advertisedDevice.getRSSI());

            // Parse manufacturer data if present
            if (advertisedDevice.haveManufacturerData()) {
                std::string data = advertisedDevice.getManufacturerData();
                if (data.length() >= 7) {
                    // Extract temperature
                    int16_t temp = (data[4] << 8) | data[3];
                    Serial.print("Temperature: ");
                    Serial.print(temp / 10.0);
                    Serial.println("°C");

                    // Extract humidity
                    uint8_t humidity = data[5];
                    Serial.print("Humidity: ");
                    Serial.print(humidity / 10.0);
                    Serial.println("%");

                    // Extract battery
                    Serial.print("Battery: ");
                    Serial.print(data[6]);
                    Serial.println("%");
                }
            }
        }
    }
};

void setup() {
    Serial.begin(115200);
    Serial.println("BLE Scanner for Sensor Beacons");

    BLEDevice::init("BLE_Scanner");
    pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    pBLEScan->setActiveScan(true);  // Active scan for scan response
    pBLEScan->setInterval(100);
    pBLEScan->setWindow(99);
}

void loop() {
    Serial.println("\nScanning...");
    BLEScanResults foundDevices = pBLEScan->start(5, false);  // 5 second scan
    pBLEScan->clearResults();
    delay(2000);
}

Key Features:

  • Standard BLE services: Uses official Environmental Sensing Service UUIDs
  • Advertisement data: Embeds sensor values in advertising packet (no connection needed)
  • Notifications: Pushes updates to connected clients
  • Low power: BLE uses 15-30 mA vs WiFi’s 160-260 mA
  • RSSI-based proximity: Estimate distance from signal strength
  • Scanner example: Companion code to receive beacon data

Exercise: Calculate battery life if beacon advertises every 1 second vs every 10 seconds. Advertising: 20 mA for 5ms.

Error Handling and Watchdog Patterns

Production ESP32 code must handle network failures, timeouts, and system crashes gracefully. This example shows robust error handling patterns.

#include <WiFi.h>
#include <PubSubClient.h>
#include <esp_task_wdt.h>

// Watchdog configuration
#define WDT_TIMEOUT 30  // 30 seconds

// WiFi Configuration
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";

// MQTT Configuration
const char* mqtt_server = "broker.hivemq.com";
WiFiClient espClient;
PubSubClient mqtt(espClient);

// Error tracking
struct ErrorStats {
    uint32_t wifiFailures = 0;
    uint32_t mqttFailures = 0;
    uint32_t sensorFailures = 0;
    uint32_t watchdogResets = 0;
    uint32_t totalResets = 0;
};

RTC_DATA_ATTR ErrorStats errors;  // Preserved across resets

// Connection timeouts
const unsigned long WIFI_TIMEOUT = 20000;      // 20 seconds
const unsigned long MQTT_TIMEOUT = 10000;      // 10 seconds
const unsigned long OPERATION_TIMEOUT = 5000;  // 5 seconds

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n=== ESP32 Production Error Handling ===");

    // Check reset reason
    checkResetReason();

    // Configure watchdog timer
    configureWatchdog();

    // Initialize with error handling
    if (!initializeWiFi()) {
        Serial.println("Critical: WiFi init failed. Entering safe mode.");
        enterSafeMode();
    }

    if (!initializeMQTT()) {
        Serial.println("Warning: MQTT init failed. Will retry.");
    }

    printErrorStats();
}

void loop() {
    // Reset watchdog timer
    esp_task_wdt_reset();

    // Ensure connections with timeout protection
    maintainConnections();

    // Process MQTT with timeout
    if (mqtt.connected()) {
        unsigned long start = millis();
        mqtt.loop();
        if (millis() - start > OPERATION_TIMEOUT) {
            Serial.println("Warning: MQTT loop timeout!");
        }
    }

    // Your application logic here
    doSensorWork();

    delay(100);
}

void configureWatchdog() {
    Serial.println("Configuring watchdog timer...");
    esp_task_wdt_init(WDT_TIMEOUT, true);  // Enable panic so ESP32 restarts
    esp_task_wdt_add(NULL);  // Add current thread to WDT watch
    Serial.println("Watchdog enabled with " + String(WDT_TIMEOUT) + "s timeout");
}

void checkResetReason() {
    esp_reset_reason_t reason = esp_reset_reason();
    errors.totalResets++;

    Serial.print("Reset reason: ");
    switch(reason) {
        case ESP_RST_POWERON:
            Serial.println("Power-on reset");
            // First boot - reset error counters
            errors.wifiFailures = 0;
            errors.mqttFailures = 0;
            errors.sensorFailures = 0;
            errors.watchdogResets = 0;
            break;
        case ESP_RST_SW:
            Serial.println("Software reset (ESP.restart())");
            break;
        case ESP_RST_PANIC:
            Serial.println("Exception/panic reset");
            break;
        case ESP_RST_INT_WDT:
        case ESP_RST_TASK_WDT:
        case ESP_RST_WDT:
            Serial.println("Watchdog reset");
            errors.watchdogResets++;
            break;
        case ESP_RST_BROWNOUT:
            Serial.println("Brownout reset (power supply issue!)");
            break;
        default:
            Serial.println("Unknown");
    }
}

bool initializeWiFi() {
    Serial.println("\n--- Initializing WiFi ---");

    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    unsigned long startAttempt = millis();

    while (WiFi.status() != WL_CONNECTED &&
           millis() - startAttempt < WIFI_TIMEOUT) {
        delay(500);
        Serial.print(".");
        esp_task_wdt_reset();  // Reset watchdog during connection
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi connected!");
        Serial.println("IP: " + WiFi.localIP().toString());
        Serial.println("RSSI: " + String(WiFi.RSSI()) + " dBm");

        // Configure WiFi for reliability
        WiFi.setAutoReconnect(true);
        WiFi.persistent(true);

        return true;
    } else {
        Serial.println("\nWiFi connection failed!");
        errors.wifiFailures++;
        return false;
    }
}

bool initializeMQTT() {
    Serial.println("\n--- Initializing MQTT ---");

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Cannot init MQTT: WiFi not connected");
        return false;
    }

    mqtt.setServer(mqtt_server, 1883);
    mqtt.setCallback(mqttCallback);
    mqtt.setKeepAlive(60);

    // Set socket timeout
    espClient.setTimeout(MQTT_TIMEOUT / 1000);

    return reconnectMQTT();
}

bool reconnectMQTT() {
    String clientId = "ESP32_" + String((uint32_t)ESP.getEfuseMac(), HEX);

    Serial.print("Connecting to MQTT...");
    unsigned long startAttempt = millis();

    bool connected = mqtt.connect(clientId.c_str());

    if (millis() - startAttempt > MQTT_TIMEOUT) {
        Serial.println("MQTT connection timeout!");
        errors.mqttFailures++;
        return false;
    }

    if (connected) {
        Serial.println("connected!");
        mqtt.subscribe("edge/commands/#");
        publishStatus("online");
        return true;
    } else {
        Serial.print("failed, rc=");
        Serial.println(mqtt.state());
        errors.mqttFailures++;
        return false;
    }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Message [");
    Serial.print(topic);
    Serial.print("]: ");

    // Safe payload handling with length limit
    const int maxPayload = 512;
    if (length > maxPayload) {
        Serial.println("Error: Payload too large!");
        return;
    }

    char message[maxPayload + 1];
    memcpy(message, payload, length);
    message[length] = '\0';

    Serial.println(message);

    // Handle special commands
    if (strcmp(topic, "edge/commands/reset") == 0) {
        Serial.println("Reset command received");
        delay(1000);
        ESP.restart();
    } else if (strcmp(topic, "edge/commands/stats") == 0) {
        publishErrorStats();
    }
}

void maintainConnections() {
    static unsigned long lastCheck = 0;

    if (millis() - lastCheck < 5000) {
        return;  // Check every 5 seconds
    }
    lastCheck = millis();

    // Check WiFi
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi disconnected!");
        if (!initializeWiFi()) {
            // Critical failure - consider safe mode
            if (errors.wifiFailures > 10) {
                Serial.println("Too many WiFi failures. Restarting...");
                delay(1000);
                ESP.restart();
            }
        }
    }

    // Check MQTT
    if (!mqtt.connected() && WiFi.status() == WL_CONNECTED) {
        Serial.println("MQTT disconnected!");
        reconnectMQTT();
    }
}

void doSensorWork() {
    static unsigned long lastRead = 0;

    if (millis() - lastRead < 10000) {
        return;  // Read every 10 seconds
    }
    lastRead = millis();

    // Simulate sensor read with timeout and error handling
    float value = readSensorWithTimeout(OPERATION_TIMEOUT);

    if (isnan(value)) {
        errors.sensorFailures++;
        Serial.println("Sensor read failed!");

        // Too many sensor failures?
        if (errors.sensorFailures > 5) {
            Serial.println("Too many sensor errors. Check hardware!");
            // Could publish alert via MQTT
        }
        return;
    }

    // Publish with error handling
    String payload = "{\"value\":" + String(value) + ",\"rssi\":" + String(WiFi.RSSI()) + "}";

    if (!mqtt.publish("edge/sensors/data", payload.c_str())) {
        Serial.println("Publish failed!");
    }
}

float readSensorWithTimeout(unsigned long timeout) {
    unsigned long start = millis();

    // Simulate sensor read (replace with actual sensor code)
    delay(100);  // Simulated I2C/SPI transaction

    if (millis() - start > timeout) {
        Serial.println("Sensor read timeout!");
        return NAN;
    }

    // Simulate successful read
    return random(200, 300) / 10.0;  // 20.0 - 30.0
}

void publishStatus(const char* status) {
    String payload = "{";
    payload += "\"status\":\"" + String(status) + "\",";
    payload += "\"uptime\":" + String(millis() / 1000) + ",";
    payload += "\"rssi\":" + String(WiFi.RSSI()) + ",";
    payload += "\"heap\":" + String(ESP.getFreeHeap());
    payload += "}";

    mqtt.publish("edge/status", payload.c_str(), true);  // Retained
}

void publishErrorStats() {
    String payload = "{";
    payload += "\"wifiFailures\":" + String(errors.wifiFailures) + ",";
    payload += "\"mqttFailures\":" + String(errors.mqttFailures) + ",";
    payload += "\"sensorFailures\":" + String(errors.sensorFailures) + ",";
    payload += "\"watchdogResets\":" + String(errors.watchdogResets) + ",";
    payload += "\"totalResets\":" + String(errors.totalResets) + ",";
    payload += "\"freeHeap\":" + String(ESP.getFreeHeap());
    payload += "}";

    mqtt.publish("edge/diagnostics", payload.c_str());
}

void printErrorStats() {
    Serial.println("\n=== Error Statistics ===");
    Serial.println("WiFi Failures: " + String(errors.wifiFailures));
    Serial.println("MQTT Failures: " + String(errors.mqttFailures));
    Serial.println("Sensor Failures: " + String(errors.sensorFailures));
    Serial.println("Watchdog Resets: " + String(errors.watchdogResets));
    Serial.println("Total Resets: " + String(errors.totalResets));
    Serial.println("Free Heap: " + String(ESP.getFreeHeap()) + " bytes");
    Serial.println("=======================\n");
}

void enterSafeMode() {
    Serial.println("\n!!! ENTERING SAFE MODE !!!");
    Serial.println("System will blink LED and wait for manual reset");

    pinMode(2, OUTPUT);
    while(true) {
        digitalWrite(2, HIGH);
        delay(500);
        digitalWrite(2, LOW);
        delay(500);
        esp_task_wdt_reset();  // Prevent watchdog reset
    }
}

Key Error Handling Features:

  1. Watchdog Timer: Automatically resets if system hangs (30s timeout)
  2. Timeout Protection: All network operations have timeouts
  3. Reset Reason Tracking: Diagnoses crash causes (brownout, watchdog, panic)
  4. Error Statistics: Tracks failures across resets using RTC memory
  5. Safe Mode: Emergency state when critical failures occur
  6. Connection Maintenance: Periodic health checks every 5 seconds
  7. Graceful Degradation: Continues operating with partial failures
  8. Memory Monitoring: Tracks heap to detect memory leaks
  9. MQTT Diagnostics: Publishes error stats for remote monitoring

Exercise: Add exponential backoff for WiFi reconnection and implement a circuit breaker pattern that stops trying after N consecutive failures.

Practical Exercises

Exercise 1: Power Consumption Analysis

Modify the deep sleep example to implement different duty cycles:

  1. High frequency: Wake every 10 seconds, active for 5 seconds
  2. Medium frequency: Wake every 1 minute, active for 10 seconds
  3. Low frequency: Wake every 5 minutes, active for 15 seconds

Calculate and measure actual battery life for each scenario using a 2000 mAh battery.

Deliverable: Table comparing theoretical vs measured battery life, including explanation of any discrepancies.

Exercise 2: MQTT Message Buffering

Enhance the WiFi+MQTT example to buffer sensor readings when WiFi is unavailable:

  1. Store up to 50 readings in RTC memory
  2. Transmit all buffered data when connection restored
  3. Implement circular buffer to prevent overflow

Deliverable: Code that successfully buffers and transmits historical data after network outage.

Exercise 3: BLE-to-WiFi Gateway

Create a system with multiple BLE sensor beacons and one WiFi gateway:

  1. BLE sensors (3+ ESP32s) advertise temperature/humidity
  2. WiFi gateway scans for beacons and aggregates data
  3. Gateway publishes all sensor data to MQTT with RSSI (for localization)

Deliverable: Complete system demonstrating multi-sensor aggregation and approximate indoor positioning based on RSSI.

Exercise 4: Robust Error Recovery

Extend the error handling example with:

  1. Exponential backoff for reconnection (1s, 2s, 4s, 8s, max 30s)
  2. Circuit breaker pattern (stop trying after 5 failures, resume after 5 minutes)
  3. Health monitoring endpoint that responds via MQTT
  4. Automatic restart if heap falls below 10KB

Deliverable: Code that survives prolonged network outages and automatically recovers without intervention.

Interactive Notebook

The notebook below contains runnable code for all Level 1 activities.

LAB 9: ESP32 Edge Node Programming and Wireless IoT

Open In Colab View on GitHub


Property Value
Book Chapter Chapter 09
Execution Levels Level 1 (Simulation) | Level 2 (Wokwi) | Level 3 (ESP32 Device)
Estimated Time 75 minutes
Prerequisites LAB 8 (Arduino basics), Basic networking concepts

Learning Objectives

  1. Understand wireless communication fundamentals (WiFi, BLE, RF propagation)
  2. Master MQTT protocol for IoT publish/subscribe messaging
  3. Design power-efficient wireless edge systems
  4. Implement mesh networking for extended coverage
  5. Build edge-cloud hybrid architectures with local processing

Theoretical Foundation: Wireless Communication for IoT

1.1 The Electromagnetic Spectrum and IoT

Wireless communication uses electromagnetic waves to transmit information. IoT devices primarily operate in the Industrial, Scientific, and Medical (ISM) bands, which are unlicensed:

Band Frequency Technology Range Data Rate Power
Sub-GHz 868/915 MHz LoRa, Sigfox 10+ km <50 kbps Very Low
2.4 GHz 2.400-2.4835 GHz WiFi, BLE, Zigbee 10-100m 1-150 Mbps Medium
5 GHz 5.150-5.825 GHz WiFi 5/6 10-50m 300+ Mbps Higher

1.2 RF Propagation: Why Signals Get Weaker

Radio signals weaken as they travel due to several physical phenomena:

Free Space Path Loss (FSPL): The fundamental spreading of electromagnetic energy:

\(FSPL(dB) = 20 \log_{10}(d) + 20 \log_{10}(f) + 20 \log_{10}\left(\frac{4\pi}{c}\right)\)

Simplified for 2.4 GHz: \(FSPL(dB) \approx 40.05 + 20 \log_{10}(d_{meters})\)

Received Signal Strength Indicator (RSSI): Measured in dBm, indicates signal quality:

RSSI (dBm) Signal Quality Practical Meaning
> -50 Excellent Very close to AP
-50 to -60 Good Reliable connection
-60 to -70 Fair Acceptable for most uses
-70 to -80 Weak May have dropouts
< -80 Poor Unreliable connection

Link Budget Equation: Determines if communication is possible:

\(P_{rx} = P_{tx} + G_{tx} - L_{path} + G_{rx}\)

Where: - \(P_{rx}\) = Received power (dBm) - \(P_{tx}\) = Transmit power (dBm) - \(G_{tx}, G_{rx}\) = Antenna gains (dBi) - \(L_{path}\) = Path loss (dB)

1.3 Implementing RF Propagation Models

Let’s implement the path loss equations and visualize how signal strength varies with distance:

💡 Alternative Approaches: Path Loss Models

Option A: Log-Distance Model (Current approach) - Pros: Simple, adjustable path loss exponent (n) for different environments - Cons: Doesn’t model shadowing (random obstacles) - Formula: PL(d) = PL(d0) + 10·n·log10(d/d0)

Option B: Two-Ray Ground Reflection Model - Pros: More accurate for outdoor/open spaces with ground bounce - Cons: Complex, requires height parameters - Use case: Drone-to-ground communication - Code: Considers direct path + ground reflected path

Option C: Free Space + Shadowing (Log-Normal) - Pros: Models random variations due to obstacles - Cons: Requires statistical measurements - Code modification: Add + np.random.normal(0, sigma_shadow) (shadow fading)

When to use each: - Use Option A (current) for quick estimates and indoor environments - Use Option B for outdoor line-of-sight at longer distances - Use Option C for realistic simulations with random obstacles

🔬 Try It Yourself: Path Loss Parameters

Parameter Current Try These Expected Effect
n (exponent) 3.0 2.0, 4.0, 6.0 Higher = faster signal decay (more obstacles)
d0 (reference) 1 m 0.1 m, 10 m Changes baseline, usually keep at 1m
frequency 2.4 GHz 900 MHz, 5 GHz Lower freq = better penetration

Experiment: Compare path loss at 2.4 GHz vs 5 GHz

for freq in [2.4e9, 5.0e9]:
    loss = free_space_path_loss(50, frequency_hz=freq)
    print(f'{freq/1e9} GHz at 50m: {loss:.1f} dB')

Result: 5 GHz has ~6 dB more loss (worse penetration, shorter range)

Section 2: ESP32 Platform Deep Dive

2.1 ESP32 Architecture

The ESP32 is a powerful dual-core microcontroller designed for IoT applications:

┌─────────────────────────────────────────────────────────────┐
│                         ESP32 SoC                          │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │   CPU 0     │  │   CPU 1     │  │   ULP Co-processor  │ │
│  │ Xtensa LX6  │  │ Xtensa LX6  │  │   (Ultra Low Power) │ │
│  │  240 MHz    │  │  240 MHz    │  │      8 MHz          │ │
│  └─────────────┘  └─────────────┘  └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    Memory                            │  │
│  │  520 KB SRAM  │  448 KB ROM  │  4 MB Flash (ext)    │  │
│  └──────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐   │
│  │   WiFi     │  │ Bluetooth  │  │    Peripherals     │   │
│  │ 802.11b/g/n│  │ Classic+BLE│  │ ADC, DAC, I2C, SPI │   │
│  │ 150 Mbps   │  │ BLE 4.2    │  │ UART, PWM, Touch   │   │
│  └────────────┘  └────────────┘  └────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2.2 ESP32 vs Arduino Comparison

Feature ESP32 Arduino Nano 33 BLE Arduino Uno
CPU Dual Xtensa LX6 @ 240MHz nRF52840 @ 64MHz ATmega328P @ 16MHz
RAM 520 KB 256 KB 2 KB
Flash 4 MB 1 MB 32 KB
WiFi ✅ 802.11 b/g/n
Bluetooth ✅ Classic + BLE ✅ BLE only
Active Current ~80-240 mA ~15-58 mA ~15-20 mA
Deep Sleep ~10 µA ~1 µA ~0.1 µA (power down)
ADC Resolution 12-bit 12-bit 10-bit
Operating Voltage 3.3V 3.3V 5V

Section 3: MQTT Protocol Deep Dive

3.1 Why MQTT for IoT?

MQTT (Message Queuing Telemetry Transport) is the de facto standard for IoT messaging:

Feature MQTT HTTP WebSocket
Pattern Pub/Sub Request/Response Bidirectional
Header Size 2 bytes min 100s of bytes 2-14 bytes
Connection Persistent Per-request Persistent
Power Low High Medium
Best For Sensors, telemetry Web APIs Real-time apps

3.2 MQTT Architecture

┌─────────────┐     ┌─────────────────────────┐     ┌─────────────┐
│  Publisher  │────▶│      MQTT Broker        │◀────│  Subscriber │
│   (ESP32)   │     │  (Mosquitto/HiveMQ)     │     │  (Server)   │
└─────────────┘     └─────────────────────────┘     └─────────────┘
      │                        │                          │
      │  PUBLISH               │                          │
      │  topic: sensors/temp   │         SUBSCRIBE        │
      │  payload: {"temp":22}  │         topic: sensors/# │
      │ ──────────────────────▶│ ─────────────────────────│
      │                        │                          │
      │                        │  PUBLISH (forwarded)     │
      │                        │ ────────────────────────▶│

3.3 MQTT Quality of Service (QoS)

MQTT provides three levels of delivery guarantee:

QoS 0: At Most Once (“Fire and Forget”)

Publisher ──PUBLISH──▶ Broker ──PUBLISH──▶ Subscriber
  • No acknowledgment, message may be lost
  • Fastest, lowest overhead
  • Use for: High-frequency telemetry where occasional loss is acceptable

QoS 1: At Least Once

Publisher ──PUBLISH──▶ Broker ──PUBLISH──▶ Subscriber
          ◀──PUBACK───       ◀──PUBACK───
  • Guaranteed delivery, may have duplicates
  • Use for: Commands, important alerts

QoS 2: Exactly Once

Publisher ──PUBLISH──▶ Broker
          ◀──PUBREC───
          ──PUBREL───▶
          ◀──PUBCOMP──
  • Guaranteed exactly-once delivery (4-way handshake)
  • Highest overhead
  • Use for: Financial transactions, critical commands

💡 Alternative Approaches: MQTT QoS Selection

Option A: QoS 0 (Fire and Forget) - Pros: Fastest, lowest overhead (2 bytes header), no acknowledgment - Cons: Message can be lost if network drops - Use case: High-frequency sensor data where occasional loss is acceptable (temp every 1s)

Option B: QoS 1 (At Least Once) - Recommended for most IoT - Pros: Guaranteed delivery, only 1 acknowledgment (PUBACK) - Cons: Possible duplicates if ACK is lost - Use case: Important events (alerts, commands) that can tolerate duplicates

Option C: QoS 2 (Exactly Once) - Pros: Guaranteed exactly-once delivery - Cons: 4-way handshake (slow), 4× more messages - Use case: Financial transactions, critical commands that must not duplicate

When to use each: - Use QoS 0 for telemetry (temperature, humidity) - OK to lose 1-2% - Use QoS 1 for alerts and commands - most common choice - Use QoS 2 only when duplicates are dangerous (e.g., “unlock door” command)

🔬 Try It Yourself: QoS Trade-offs

Parameter QoS 0 QoS 1 QoS 2 Impact
Messages sent 1 2 (PUB+ACK) 4 Network load
Latency ~50ms ~100ms ~200ms Response time
Battery cost Power drain

Experiment: Simulate message loss and recovery

# Simulate 5% packet loss
for qos in [0, 1, 2]:
    delivered = 0
    for _ in range(100):
        if np.random.random() > 0.05:  # 95% success
            delivered += 1
    print(f'QoS {qos}: {delivered}% delivered')

Result: QoS 0 loses messages, QoS 1/2 retry until successful

⚠️ Common Issues and Debugging

If ESP32 won’t connect to WiFi: - Check: Is SSID correct (case-sensitive)? → Use WiFi.scanNetworks() to list available - Check: Is WiFi password correct? → ESP32 will fail silently with wrong password - Check: Is router on 2.4 GHz? → ESP32 doesn’t support 5 GHz WiFi - Check: RSSI too weak? → Move closer to AP, check antenna connection - Diagnostic: Print WiFi.status() to see error code

If MQTT connection fails: - Check: Is broker reachable? → Ping broker IP from ESP32’s network - Check: Is port 1883 open? → Some networks block MQTT, try port 443 (MQTT over TLS) - Check: Does broker require authentication? → Add username/password in client.connect() - Check: Client ID conflict? → Use unique ID like "ESP32_" + WiFi.macAddress() - Diagnostic: Enable debug with client.setDebugOutput(true)

If battery drains quickly: - Check: Is ESP32 in constant WiFi mode? → Use deep sleep between transmissions - Check: Is transmit power too high? → Lower with WiFi.setTxPower(WIFI_POWER_11dBm) - Expected: Active WiFi = 150mA, deep sleep = 10µA (15,000× difference!) - Formula: Battery life (hours) = Capacity (mAh) / Average Current (mA)

If messages are delayed/dropped: - Check: Is WiFi signal weak (RSSI < -70 dBm)? → Move closer or add external antenna - Check: Is broker overloaded? → Check broker logs for queuing - Check: Is network congested? → Use WiFi analyzer app to find cleaner channel - Diagnostic: Measure round-trip time with ping command

ESP32-specific gotchas: - GPIO12 must be LOW during boot (or device won’t start) - WiFi uses pins 6-11 internally (don’t use for sensors) - analogRead() resolution is 12-bit (0-4095, not 0-1023 like Arduino) - After deep sleep, RTC memory persists but RAM is lost

Section 4: Power Management for IoT

4.1 Battery Life Calculation

For battery-powered IoT devices, power efficiency is critical. The battery life depends on:

\(\text{Battery Life (hours)} = \frac{\text{Battery Capacity (mAh)}}{\text{Average Current (mA)}}\)

For duty-cycled operation:

\(I_{avg} = (I_{active} \times t_{active} + I_{sleep} \times t_{sleep}) / (t_{active} + t_{sleep})\)

4.2 Duty Cycling Strategy

Power
  ▲
  │    ┌───┐                          ┌───┐
  │    │Tx │                          │Tx │
240mA─┼────┤   │                          │   │
  │    │   │                          │   │
  │    │   │                          │   │
 30mA─┼───┬┤   ├──┐                   ┌─┤   ├──┐
  │   │   │   │  │                   │ │   │  │
0.01mA┼───┴───┴───┴──────────────────┴─┴───┴──┴──────▶ Time
  │   │Wake│Send│     Deep Sleep       │Repeat
  │   │ up │    │    (60 seconds)      │

Section 5: Edge-Cloud Hybrid Architecture

5.1 Why Process at the Edge?

Factor Cloud Processing Edge Processing
Latency 100-500ms (network RTT) <10ms (local)
Bandwidth Sends all raw data Sends only results
Privacy Data leaves device Data stays local
Reliability Needs connectivity Works offline
Cost Cloud compute charges One-time hardware

5.2 Data Reduction at Edge

Raw data volume: A sensor sampling at 100 Hz generates: \(100 \text{ samples/s} \times 4 \text{ bytes} \times 86400 \text{ s/day} = 34.5 \text{ MB/day}\)

With edge aggregation (1 summary/minute): \(1440 \text{ summaries} \times 50 \text{ bytes} = 72 \text{ KB/day}\)

Compression ratio: 99.8% bandwidth reduction!

Section 6: BLE Mesh Networking

6.1 Why Mesh Networks?

Traditional star topology (all nodes connect to single gateway) has limitations: - Range: Each node must reach the gateway - Reliability: Gateway failure = network failure - Scalability: Gateway becomes bottleneck

Mesh networking solves these by allowing nodes to relay messages:

Star Topology:                    Mesh Topology:
                                  
    [N1]    [N2]                  [N1]────[N2]────[N3]
       \    /                       │\    /│\    /│
        [GW]                        │ \  / │ \  / │
       /    \                       │  \/  │  \/  │
    [N3]    [N4]                  [N4]────[N5]────[N6]

6.2 Bluetooth Mesh Protocol

BLE Mesh uses managed flooding with these features:

  1. TTL (Time To Live): Limits hop count to prevent infinite loops
  2. Message Cache: Nodes remember recent messages to avoid re-broadcasting
  3. Relay Nodes: Only designated nodes forward messages

\(\text{Coverage Radius} = \text{Single Node Range} \times \text{TTL}\)

Section 7: Real ESP32 Code Reference

The following code snippets are for Level 3 deployment on actual ESP32 hardware:

7.1 WiFi + MQTT with Deep Sleep

#include <WiFi.h>
#include <PubSubClient.h>

// Configuration
const char* ssid = "YourNetwork";
const char* password = "YourPassword";
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
const int SLEEP_SECONDS = 60;

WiFiClient espClient;
PubSubClient mqtt(espClient);

void setup() {
    Serial.begin(115200);
    
    // Connect WiFi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nWiFi connected");
    
    // Connect MQTT
    mqtt.setServer(mqtt_server, mqtt_port);
    while (!mqtt.connected()) {
        String clientId = "ESP32-" + String(random(0xffff), HEX);
        if (mqtt.connect(clientId.c_str())) {
            Serial.println("MQTT connected");
        } else {
            delay(1000);
        }
    }
    
    // Read and publish sensor
    float temperature = readTemperature();
    String payload = "{\"temp\":" + String(temperature) + "}";
    mqtt.publish("sensors/esp32/temperature", payload.c_str());
    mqtt.loop();
    delay(100);  // Ensure message sent
    
    // Enter deep sleep
    Serial.printf("Sleeping for %d seconds...\n", SLEEP_SECONDS);
    esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}

void loop() {
    // Never reached - we deep sleep in setup
}

7.2 BLE Beacon (Low Power)

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEBeacon.h>

void setup() {
    BLEDevice::init("ESP32_Sensor");
    BLEAdvertising *advertising = BLEDevice::getAdvertising();
    
    // Configure beacon with sensor data
    BLEBeacon beacon;
    beacon.setManufacturerId(0x4C00);  // Apple iBeacon format
    
    // Embed sensor reading in minor value
    float temp = readTemperature();
    beacon.setMinor((int)(temp * 100));  // 22.5°C -> 2250
    
    advertising->start();
    delay(100);  // Advertise briefly
    advertising->stop();
    
    // Deep sleep for 10 seconds
    esp_deep_sleep(10 * 1000000ULL);
}

Section 7: MQTT Client Implementation (Python)

For actual deployment, you’ll use MQTT libraries. Here’s how to implement with paho-mqtt.

Section 8: HTTP API Client for Cloud Integration

When MQTT isn’t available, HTTP REST APIs provide an alternative for cloud communication.

Section 9: JSON Message Design for IoT

Efficient JSON design is critical for bandwidth-constrained IoT devices.

Section 10: Network Simulation with Packet Loss and Latency

Real networks are unreliable. Let’s simulate realistic conditions.

Section 11: Complete IoT Communication Pipeline

Putting it all together: sensor data collection, formatting, and transmission.

Checkpoint: Self-Assessment

Conceptual Questions

  1. RF Propagation: Why does a 2.4 GHz signal have shorter range than a 900 MHz signal at the same power?

  2. MQTT QoS: A fire alarm system needs to guarantee alert delivery. Which QoS level should it use and why?

  3. Power Budget: An ESP32 with a 2000mAh battery sends data every 5 minutes with 2 seconds of wake time. Estimate the battery life.

  4. Edge Processing: What are three types of data processing that should happen at the edge rather than the cloud?

  5. Mesh Networks: In a BLE mesh with TTL=3, what is the maximum distance a message can travel (assuming 10m per hop)?

Hands-On Challenges

  1. Modify the path loss model to include wall attenuation (add 3dB loss per wall crossed)

  2. Add QoS 2 simulation to the MQTT broker with full 4-way handshake

  3. Implement message retry in the ESP32 simulator when RSSI is weak

  4. Create an offline-first edge gateway that queues messages during connectivity loss


Level 2: Try the Wokwi Simulator

Test real ESP32 WiFi code without hardware:

→ ESP32 WiFi Simulation


Part of the Edge Analytics Lab Book

Three-Tier Activities

Run the embedded notebook above. Key exercises:

  1. Follow along with the code cells
  2. Modify parameters and observe results
  3. Complete the checkpoint questions

ESP32 WiFi & IoT Simulator

Test WiFi and IoT code in your browser using Wokwi:

  • Real WiFi connectivity simulation
  • HTTP requests to live web services
  • Sensor data transmission
  • Connection resilience exercises

Complete IoT sensor node

Visual Troubleshooting

ESP32 WiFi Connection Issues

flowchart TD
    A[WiFi won't connect] --> B{WiFi.status?}
    B -->|WL_NO_SSID_AVAIL| C[Network not found:<br/>Check SSID spelling<br/>Must be 2.4GHz not 5GHz<br/>Move closer to router]
    B -->|WL_CONNECT_FAILED| D[Auth failed:<br/>Verify password<br/>Check WPA2 security<br/>Router MAC filter?]
    B -->|WL_DISCONNECTED| E{Connects then drops?}
    E -->|Yes| F[Weak signal:<br/>Move closer to AP<br/>Add external antenna<br/>Check power supply]
    E -->|Never connects| G[Add timeout loop:<br/>while status != CONNECTED<br/>delay 500 retry 20x]
    B -->|WL_IDLE_STATUS| H[Not initialized:<br/>WiFi.mode WIFI_STA<br/>before WiFi.begin]

    style A fill:#ff6b6b
    style C fill:#4ecdc4
    style D fill:#4ecdc4
    style F fill:#4ecdc4
    style G fill:#4ecdc4
    style H fill:#4ecdc4

For complete troubleshooting flowcharts, see: