Back to Sensors & Actuators Series

Part 11: IoT & Connected Systems

July 14, 2025 Wasil Zafar 45 min read

IoT architecture layers, wireless protocols, MQTT messaging, cloud platform integration, TLS security, and Over-the-Air firmware updates for connected sensor networks.

Table of Contents

  1. IoT Architecture
  2. Wireless Protocols
  3. MQTT Protocol
  4. Cloud Integration
  5. Security
  6. OTA Updates
  7. Conclusion & Next Steps

IoT Architecture

Three-Tier Model

IoT Architecture Layers:
  • Perception Layer (Edge): Sensors, actuators, microcontrollers. Collects raw data, performs local processing, executes control actions
  • Network Layer (Fog): Gateways, routers, protocol translators. Aggregates data, provides local intelligence, bridges protocols (BLE → MQTT → Cloud)
  • Application Layer (Cloud): Cloud platforms, databases, analytics, dashboards. Long-term storage, ML training, fleet management, alerting
IoT Three-Tier Architecture
flowchart TD
    subgraph Cloud["☁️ Application Layer (Cloud)"]
        DB["Database &
Analytics"] DASH["Dashboards &
Alerts"] ML["ML Training &
Fleet Mgmt"] end subgraph Network["🌐 Network Layer (Fog/Gateway)"] GW["Protocol
Translation"] AGG["Data
Aggregation"] EDGE["Local
Intelligence"] end subgraph Perception["🔌 Perception Layer (Edge)"] S1["Temperature
Sensor"] S2["Motion
Sensor"] S3["Camera
Module"] MCU["MCU /
SBC"] end Perception -->|"MQTT / CoAP
BLE / LoRa"| Network Network -->|"HTTPS / AMQP"| Cloud Cloud -.->|"Commands /
OTA Updates"| Network Network -.->|"Config /
Firmware"| Perception style Cloud fill:#e8f4f4,stroke:#3B9797,color:#132440 style Network fill:#f0f4f8,stroke:#16476A,color:#132440 style Perception fill:#e8f4f4,stroke:#3B9797,color:#132440

Edge Computing

Edge computing processes data locally on the device or gateway rather than sending everything to the cloud. This reduces latency (ms vs seconds), bandwidth costs, and cloud dependency while improving privacy and enabling real-time control.

Edge vs Cloud Processing

FactorEdgeCloud
Latency<10 ms100 ms–seconds
BandwidthMinimal (processed data)High (raw data)
ReliabilityWorks offlineRequires connectivity
Compute powerLimited (MCU resources)Virtually unlimited
StorageKB–MB (flash)Unlimited
ML capabilityInference only (TinyML)Training + inference

Wireless Protocols

Protocol Comparison

IoT Wireless Protocol Comparison

ProtocolRangeData RatePowerUse Case
BLE 5.0100 m2 MbpsVery LowWearables, beacons, proximity
WiFi50 m11–600 MbpsHighHome automation, cameras
Zigbee100 m250 kbpsLowMesh networks, smart home
LoRaWAN15 km0.3–50 kbpsVery LowAgriculture, asset tracking
NB-IoT10 km250 kbpsLowSmart meters, city infrastructure
Thread100 m250 kbpsLowIPv6 mesh, Matter smart home

LoRaWAN

LoRaWAN is ideal for battery-powered sensors that need to send small payloads over long distances. A single gateway can serve thousands of devices across several kilometers.

// LoRaWAN OTAA join and sensor data transmission (STM32 + SX1276)
#include "lora.h"
#include "sensor.h"

// OTAA credentials (from LoRaWAN network server)
static const uint8_t DEV_EUI[]  = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
static const uint8_t APP_EUI[]  = {0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x00, 0x00, 0x01};
static const uint8_t APP_KEY[]  = {0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
                                   0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C};

void lora_sensor_task(void) {
    // Join network via OTAA
    lora_config_otaa(DEV_EUI, APP_EUI, APP_KEY);
    if (lora_join() != LORA_OK) {
        // Retry with exponential backoff
        return;
    }

    // Read sensor data
    float temperature = sensor_read_temperature();
    float humidity    = sensor_read_humidity();
    uint16_t battery  = sensor_read_battery_mv();

    // Pack payload (Cayenne LPP format for easy decoding)
    uint8_t payload[11];
    payload[0] = 0x01;  // Channel 1
    payload[1] = 0x67;  // Temperature type
    payload[2] = (int16_t)(temperature * 10) >> 8;
    payload[3] = (int16_t)(temperature * 10) & 0xFF;
    payload[4] = 0x02;  // Channel 2
    payload[5] = 0x68;  // Humidity type
    payload[6] = (uint8_t)(humidity * 2);
    payload[7] = 0x03;  // Channel 3
    payload[8] = 0x02;  // Analog input type
    payload[9] = battery >> 8;
    payload[10] = battery & 0xFF;

    // Send on port 1, unconfirmed
    lora_send(1, payload, sizeof(payload), false);
}

MQTT Protocol

Core Concepts

MQTT Fundamentals:
  • Publish/Subscribe: Decoupled messaging. Publishers don't know about subscribers
  • Broker: Central server (Mosquitto, HiveMQ, AWS IoT) routes messages between clients
  • Topics: Hierarchical namespace: sensors/building-a/floor-2/temperature
  • QoS Levels: 0 (at most once), 1 (at least once), 2 (exactly once)
  • Retained Messages: Broker stores last message per topic for new subscribers
  • Last Will: Message published automatically when client disconnects unexpectedly
  • Keep Alive: Periodic PINGREQ/PINGRESP to detect broken connections
MQTT Publish/Subscribe Flow
sequenceDiagram
    participant D as 🔌 Device
    participant B as 📡 Broker
    participant S1 as 📊 Dashboard
    participant S2 as 💾 Cloud DB
    D->>B: CONNECT (ClientID, Last Will)
    B-->>D: CONNACK
    S1->>B: SUBSCRIBE (sensors/temp/#)
    S2->>B: SUBSCRIBE (sensors/+/data)
    B-->>S1: SUBACK
    B-->>S2: SUBACK
    loop Every 30 seconds
        D->>B: PUBLISH (sensors/temp/room1, QoS 1)
        B-->>D: PUBACK
        B->>S1: Forward message
        B->>S2: Forward message
    end
    Note over D,B: Device disconnects unexpectedly
    B->>S1: Last Will message (device offline)
    B->>S2: Last Will message (device offline)
                            

Implementation

# MQTT sensor publisher using paho-mqtt (ESP32/RPi)
import paho.mqtt.client as mqtt
import json
import time
import ssl

BROKER = "your-broker.example.com"
PORT = 8883  # TLS
TOPIC = "sensors/greenhouse/zone-1/environment"

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected to MQTT broker")
        # Subscribe to commands
        client.subscribe("commands/greenhouse/zone-1/#")
    else:
        print(f"Connection failed: {rc}")

def on_message(client, userdata, msg):
    print(f"Command received: {msg.topic} -> {msg.payload.decode()}")
    command = json.loads(msg.payload)
    if command.get("action") == "irrigate":
        activate_pump(command.get("duration", 30))

# Configure TLS
client = mqtt.Client(client_id="sensor-node-001")
client.tls_set(
    ca_certs="ca.pem",
    certfile="client-cert.pem",
    keyfile="client-key.pem",
    tls_version=ssl.PROTOCOL_TLSv1_2
)
client.on_connect = on_connect
client.on_message = on_message

# Last Will message (sent if client disconnects unexpectedly)
client.will_set(
    "status/greenhouse/zone-1",
    payload=json.dumps({"status": "offline"}),
    qos=1, retain=True
)

client.connect(BROKER, PORT)
client.loop_start()

while True:
    payload = json.dumps({
        "temperature": read_temperature(),
        "humidity": read_humidity(),
        "soil_moisture": read_soil_moisture(),
        "timestamp": time.time()
    })
    client.publish(TOPIC, payload, qos=1)
    time.sleep(60)

Cloud Integration

AWS IoT Core

AWS IoT Core Features:
  • Device Shadow: JSON document representing device state. Syncs desired vs reported state
  • Rules Engine: SQL-like queries on MQTT messages. Route to Lambda, DynamoDB, S3, SNS
  • Device Defender: Monitors device behavior, detects anomalies
  • Fleet Provisioning: Automatic certificate generation for manufacturing at scale
  • MQTT + X.509: Mutual TLS authentication with per-device certificates

Azure IoT Hub

Azure IoT Hub Features:
  • Device Twins: Similar to AWS Shadows — desired and reported properties
  • Direct Methods: Synchronous request/response to devices (reboot, configure)
  • Message Routing: Route telemetry to Event Hubs, Blob Storage, Service Bus, Cosmos DB
  • IoT Edge: Run cloud workloads on gateway devices (Docker containers)
  • DPS: Device Provisioning Service for zero-touch enrollment

Security

TLS & Certificates

IoT Security Essentials:
  • Mutual TLS (mTLS): Both client and server present certificates. Standard for AWS/Azure IoT
  • Unique device identity: Each device gets its own X.509 certificate (never share keys)
  • Certificate rotation: Plan for periodic certificate renewal before expiry
  • Secure element: Store private keys in tamper-resistant hardware (ATECC608, SE050)
  • Least privilege: IoT policies should restrict topics a device can publish/subscribe to
  • Firmware signing: Cryptographically sign firmware images; verify signature before flashing
Secure Boot Verification Flow
flowchart TD
    A["⚡ Power On"] --> B["ROM Bootloader
(Immutable)"] B --> C{"Verify 1st-Stage
Bootloader Signature?"} C -->|"Valid ✅"| D["1st-Stage Bootloader"] C -->|"Invalid ❌"| FAIL["🔒 Recovery Mode"] D --> E{"Verify Firmware
Signature?"} E -->|"Valid ✅"| F{"Anti-Rollback
Version Check?"} E -->|"Invalid ❌"| FAIL F -->|"Pass ✅"| G{"CRC / Hash
Integrity Check?"} F -->|"Fail ❌"| FAIL G -->|"Pass ✅"| H["🚀 Application Runs"] G -->|"Fail ❌"| FAIL style A fill:#3B9797,stroke:#3B9797,color:#fff style H fill:#132440,stroke:#132440,color:#fff style FAIL fill:#fff5f5,stroke:#BF092F,color:#132440 style C fill:#e8f4f4,stroke:#3B9797,color:#132440 style E fill:#e8f4f4,stroke:#3B9797,color:#132440 style F fill:#f0f4f8,stroke:#16476A,color:#132440 style G fill:#f0f4f8,stroke:#16476A,color:#132440

Secure Boot Chain

// Simplified secure boot verification flow
#include <stdint.h>
#include <stdbool.h>
#include "crypto.h"

#define FW_START_ADDR    0x08010000  // Application start in flash
#define FW_MAX_SIZE      (256 * 1024)
#define SIG_OFFSET       (FW_MAX_SIZE - 256)  // RSA-2048 signature

// Public key embedded in bootloader (read-only, protected by RDP)
extern const uint8_t OEM_PUBLIC_KEY[256];

typedef struct {
    uint32_t magic;       // 0xDEADBEEF
    uint32_t version;     // Firmware version (monotonic)
    uint32_t size;        // Firmware size in bytes
    uint32_t crc32;       // CRC32 of firmware body
    uint8_t  signature[256];  // RSA-2048 signature
} fw_header_t;

bool verify_firmware(void) {
    fw_header_t *hdr = (fw_header_t *)FW_START_ADDR;

    // 1. Check magic number
    if (hdr->magic != 0xDEADBEEF)
        return false;

    // 2. Verify anti-rollback (version must be >= stored minimum)
    uint32_t min_version = read_otp_min_version();
    if (hdr->version < min_version)
        return false;

    // 3. Verify CRC32 of firmware body
    uint32_t calc_crc = crc32_compute(
        (uint8_t *)(FW_START_ADDR + sizeof(fw_header_t)),
        hdr->size
    );
    if (calc_crc != hdr->crc32)
        return false;

    // 4. Verify RSA signature over header + firmware
    bool sig_valid = rsa_verify(
        OEM_PUBLIC_KEY,
        (uint8_t *)FW_START_ADDR,
        sizeof(fw_header_t) + hdr->size - 256,
        hdr->signature
    );

    return sig_valid;
}

// Bootloader entry point
void bootloader_main(void) {
    if (verify_firmware()) {
        // Jump to application
        uint32_t app_sp = *(volatile uint32_t *)(FW_START_ADDR + sizeof(fw_header_t));
        uint32_t app_pc = *(volatile uint32_t *)(FW_START_ADDR + sizeof(fw_header_t) + 4);
        __set_MSP(app_sp);
        ((void (*)(void))app_pc)();
    } else {
        // Stay in bootloader, wait for valid firmware via UART/USB
        enter_recovery_mode();
    }
}

OTA Firmware Updates

OTA Update Architecture (A/B Partitioning):
  • Partition A (Active): Currently running firmware
  • Partition B (Staging): New firmware is written here during download
  • Bootloader: Decides which partition to boot based on metadata flags
  • Rollback: If new firmware fails health check (watchdog, self-test), bootloader reverts to previous partition
  • Delta updates: Send only binary diff (bsdiff) to reduce OTA payload size by 60–90%
OTA A/B Partition Update Flow
stateDiagram-v2
    [*] --> Running_A : Boot from Partition A
    Running_A --> Downloading : OTA Update Available
    Downloading --> Verifying : Download to Partition B
    Verifying --> Ready_B : Signature Valid ✅
    Verifying --> Running_A : Signature Invalid ❌
    Ready_B --> Rebooting : Set Boot Flag → B
    Rebooting --> Health_Check : Bootloader Loads B
    Health_Check --> Running_B : Self-test Pass ✅
    Health_Check --> Rollback : Self-test Fail ❌
    Rollback --> Running_A : Revert to Partition A
    Running_B --> [*] : Mark B Active
                            

Conclusion & Next Steps

Connected sensor systems bridge the physical and digital worlds. The right wireless protocol, secure MQTT messaging, cloud integration, and OTA update capability transform standalone embedded devices into managed IoT fleets that can be monitored, controlled, and updated remotely throughout their lifecycle.

Key Takeaways:
  • Choose wireless protocol based on range, bandwidth, and power requirements
  • MQTT with QoS 1 provides reliable sensor telemetry with minimal overhead
  • Always use mutual TLS with unique per-device certificates
  • Store private keys in secure elements, never in unprotected flash
  • A/B partitioning with rollback is essential for safe OTA updates

In Part 12, we cover Professional & Industry-Level Skills — functional safety standards (ISO 26262, IEC 61508), embedded Linux, DSP fundamentals, and FPGA introduction.