🎬 Watch the Full Demo

After 12 weeks of building, here’s the result: a production-grade Industrial IoT monitoring platform built from scratch.

Industrial IoT Monitoring Platform

▢️ Watch on YouTube (6 minutes)


What I Built

A complete Industrial IoT monitoring platform demonstrating expertise across the full stack:

Hardware (4 Sensor Nodes)

LoRaWAN Nodes (STM32WL55):

  • LoRa-1: SHT41 temperature/humidity sensor + SSD1306 OLED
  • LoRa-2: BME680 environmental sensor + SH1106 OLED
  • Native LoRa radio (no external modules)
  • AU915 frequency band, OTAA join

Modbus TCP Nodes (STM32F446):

  • Modbus-1: SHT41 sensor + W5500 Ethernet
  • Modbus-2: BME680 sensor + W5500 Ethernet
  • IEEE 754 float32 register encoding
  • 40001-40007 holding register map

Software Stack

Firmware: Embedded Rust + Embassy async runtime
Protocols: LoRaWAN (OTAA), Modbus TCP, MQTT
Infrastructure: Docker Compose, InfluxDB 2.x, Grafana 10.x
Gateway: RAK7268V2 WisGate Edge Lite 2


System Architecture

System Architecture

The platform integrates two industrial protocols (LoRaWAN and Modbus TCP) into a unified monitoring dashboard. Data flows from sensors through protocol-specific bridges into InfluxDB, with real-time visualization in Grafana.

Key architectural decisions:

  • Embassy async framework (no RTOS overhead)
  • Push-based MQTT (better fit than Prometheus pull)
  • Docker Compose (reproducible infrastructure)
  • Unified InfluxDB bucket with protocol tagging

View full system architecture β†’


Data Flow Pipeline

Data Flow Pipeline

End-to-end latency: < 2 seconds

  1. Sensor Read (10-15ms): I2C communication with SHT41/BME680
  2. Encode Payload (1ms): Integer-only math, big-endian byte order
  3. LoRa TX (1.3-1.5s): SF7-SF12 spreading factor, 915 MHz
  4. Gateway Receive (<100ms): RAK7268V2 processes and publishes to MQTT
  5. Bridge Decode (50ms): Python script decodes Base64, parses JSON
  6. InfluxDB Write (20ms): Line protocol over HTTP
  7. Grafana Display (poll): Real-time dashboard with 15+ panels

View detailed data flow β†’


Network Topology

Network Topology

Physical layout:

  • LoRaWAN: Wireless 915.2-916.6 MHz (AU915 sub-band 2)
  • Modbus: Wired Ethernet (10.10.10.10, 10.10.10.20)
  • Gateway: 10.10.10.254 (MQTT broker :1883)
  • Docker network: Bridge mode for service isolation

View network topology β†’


Technical Highlights

1. Embassy Async Runtime (No RTOS)

Concurrent tasks without traditional RTOS overhead:

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    spawner.spawn(sensor_task(sensor)).unwrap();
    spawner.spawn(display_task(display)).unwrap();
    spawner.spawn(radio_task(radio)).unwrap();
}

#[embassy_executor::task]
async fn sensor_task(mut sensor: Sht41) {
    loop {
        let reading = sensor.read().await;
        SENSOR_DATA.signal(reading);
        Timer::after_secs(30).await;
    }
}

Embassy compiles async/await into state machines - zero stack-per-task overhead.


2. LoRaWAN Byte Order Debugging

The bug: Devices wouldn’t join the network. Gateway showed wrong DevEUI.

The fix: LoRaWAN transmits EUIs little-endian, but I stored them big-endian.

// WRONG (what I initially had):
const DEV_EUI: [u8; 8] = [0x23, 0xCE, 0x1B, 0xFE, 0xFF, 0x09, 0x1F, 0xAC];

// CORRECT (reversed for LSB-first transmission):
const DEV_EUI: [u8; 8] = [0xAC, 0x1F, 0x09, 0xFF, 0xFE, 0x1B, 0xCE, 0x23];

Lesson: Always verify wire format against specification. The “obvious” byte order is often wrong.


3. Integer-Only Sensor Math (No FPU)

Challenge: STM32WL55 has no floating-point unit. Using f32 causes HardFault.

Solution: Fixed-point integer math with Γ—100 scaling.

// SHT41 temperature: T = -45 + (175 Γ— raw) / 65535
fn convert_temperature(raw: u16) -> i16 {
    let temp_scaled = -4500i32 + ((17500i32 * raw as i32) / 65535);
    (temp_scaled / 100) as i16  // Result in Β°C Γ— 100
}

// Payload: 2 bytes big-endian
let temp_bytes = (temp_celsius as i16).to_be_bytes();
payload[0..2].copy_from_slice(&temp_bytes);

Trade-off: 0.01Β°C resolution is more than sufficient for environmental monitoring.


4. Modbus TCP Server Implementation

IEEE 754 float32 encoding in holding registers:

// Write temperature (registers 40001-40002)
let temp_bytes = temp_celsius.to_be_bytes();
registers[0] = u16::from_be_bytes([temp_bytes[0], temp_bytes[1]]);
registers[1] = u16::from_be_bytes([temp_bytes[2], temp_bytes[3]]);

// Client decodes with:
// bytes = [reg_high_MSB, reg_high_LSB, reg_low_MSB, reg_low_LSB]
// float = struct.unpack('>f', bytes)

Python bridge polls every 2 seconds and writes to InfluxDB with proper tagging.


Performance Metrics

MetricValue
End-to-End Latency< 2 seconds
LoRaWAN Join Time~7 seconds (OTAA)
LoRa TX Duration1.3-1.5 seconds (SF7-SF12)
Sensor Read Time10-15 ms (I2C @ 100kHz)
Flash Usage (STM32WL55)28 KB / 256 KB (11%)
RAM Usage (STM32WL55)8 KB / 64 KB (12.5%)
Grafana Panels15+ (temperature, humidity, pressure, link quality)
Uptime72+ hours continuous operation

Case Study & Deep Dives

I’ve documented the technical challenges, architectural decisions, and lessons learned in a comprehensive case study:

πŸ“„ Read the Full Case Study β†’

Topics covered:

  • Embassy async framework internals
  • LoRaWAN byte order discovery process
  • Integer-only sensor math (no FPU)
  • I2C peripheral sharing patterns
  • Patching dependency version conflicts
  • MQTT protocol version negotiation
  • Architecture decision rationale
  • Performance analysis
  • What I’d do differently

Repository Structure

All code is open source and documented:

4-month-plan/
β”œβ”€β”€ wk1-rtic-lora/           # RTIC + LoRa UART foundation
β”œβ”€β”€ wk2-lora-sensor-fusion/  # Multi-sensor I2C + LoRa
β”œβ”€β”€ wk3-binary-protocol/     # Postcard + CRC + ACK/retry
β”œβ”€β”€ wk5-gateway-firmware/    # JSON telemetry gateway
β”œβ”€β”€ wk6-async-gateway/       # Tokio async service
β”œβ”€β”€ wk7-mqtt-influx/         # MQTT β†’ InfluxDB β†’ Grafana
β”œβ”€β”€ wk9-opcua-modbus/        # W5500 + Modbus + OPC-UA
β”œβ”€β”€ wk10-lorawan/            # LoRaWAN sensor network
β”œβ”€β”€ wk11-unified-monitoring/ # 🌟 Self-contained unified platform
└── wk12-portfolio/          # Documentation + diagrams + video

πŸ”— View on GitHub β†’


Quick Start (Unified Platform)

The wk11-unified-monitoring repository is self-contained and deployable:

# Clone
git clone https://github.com/mapfumo/wk11-unified-monitoring
cd wk11-unified-monitoring

# Start infrastructure
docker compose up -d

# Flash firmware (requires hardware + probe-rs)
cd firmware/lorawan/lora-1 && cargo run --release
cd firmware/modbus && cargo run --release --bin modbus_1

# Access Grafana
open http://localhost:3000  # admin/admin

# Watch decoded sensor readings
python3 mqtt_subscriber.py

Requirements:

  • Docker + Docker Compose
  • Rust toolchain (for firmware)
  • probe-rs (for flashing)
  • Hardware: STM32 boards, RAK7268V2 gateway

Security Analysis

I conducted STRIDE threat modeling to identify vulnerabilities and mitigations:

Threats identified:

  • Unencrypted MQTT (Tampering)
  • Default credentials (Elevation of Privilege)
  • No network segmentation (Lateral Movement)
  • Missing TLS (Information Disclosure)
  • No rate limiting (Denial of Service)

πŸ“„ Read Full STRIDE Analysis β†’

This demonstrates production-grade security thinking - not just building features, but thinking about deployment risks.


Lessons Learned

What Worked Well

  1. Baby steps approach: Incremental changes with verification prevented cascading failures
  2. Reference projects: Starting with working examples saved hours of hardware debugging
  3. Documentation discipline: TROUBLESHOOTING.md files paid dividends when revisiting old code
  4. RTT debugging: probe-rs with Real-Time Transfer is essential for embedded work

What I’d Do Differently

  1. Start with unified repo: Migrating firmware between repos required path fixes
  2. Add TLS earlier: Security should be designed in, not bolted on
  3. Hardware-in-the-loop CI: No automated testing for firmware
  4. Power profiling: Should have measured battery consumption for LoRaWAN nodes

Key Technical Insights

  • Specification documents are authoritative: Blog posts omit critical details
  • Embedded Rust ecosystem moves fast: Expect to patch dependencies
  • Condenser mics need quiet rooms: Good choice for my timber house
  • Audio quality > video quality: Viewers tolerate poor video, not poor audio

Project Statistics

MetricValue
Duration12 weeks (part-time)
Lines of Rust~3,500
Lines of Python~800
Docker Services6 (InfluxDB, Grafana, Mosquitto, 2Γ— bridges, Chronograf)
Documentation~15,000 words
Diagrams3 (Excalidraw source files)
GitHub Repos10 (weekly progression + unified)
Blog Posts12 (weekly updates)
Video Length6 minutes
Total Investment$130 (Fifine mic + DiCUNO LEDs)

Technical Resources

Documentation

Diagrams (Excalidraw Source)

Code


Reflections

From Network Engineer to Embedded Systems Engineer

This 12-week journey proves that systematic learning + consistent shipping beats endless tutorials.

What I knew before (network engineering, 10+ years):

  • TCP/IP, routing, switching
  • Linux administration
  • Systems thinking and troubleshooting

What I learned (embedded systems, 12 weeks):

  • Bare-metal firmware in Rust
  • Industrial protocols (Modbus, LoRaWAN, OPC-UA)
  • Async runtimes without RTOS
  • Hardware debugging (logic analyzer, probe-rs, RTT)
  • Time-series databases and observability

The common thread: Systems thinking. Network engineering taught me to debug complex distributed systems - embedded systems are just smaller, closer to hardware.

The Power of Shipping

I didn’t spend 12 weeks reading books and watching videos. I spent 12 weeks building and shipping.

  • Week 1: Shipped LED blink
  • Week 2: Shipped sensor integration
  • Week 3: Shipped binary protocol
  • Week 11: Shipped unified platform
  • Week 12: Shipped portfolio video

Every week, something worked. That’s the difference between learning and doing.


This portfolio exists because I committed to shipping every week, no matter what.

Looking for opportunities in embedded systems, IIoT, or industrial automation.