Arduino firmware for streaming sensor telemetry to Azure IoT Hub via MQTT/TLS. Credentials are injected automatically from Terraform — no manual copy-paste.
The firmware is a Mustache template rendered by a PowerShell script that reads credentials directly from Terraform outputs and Azure CLI:
firmware/GlobalAzureKit/GlobalAzureKit.mustache ← Mustache template (in git)
│
│ configure-sketch.ps1
│ reads: terraform output + az iot hub device-identity
▼
firmware/output/GlobalAzureKit/GlobalAzureKit.ino ← ready to flash (gitignored)
No credentials are ever committed to git. The generated sketch lives in firmware/output/ which is gitignored.
| Sensor | JSON payload |
|---|---|
| BMP280 | {"temperature": 24.35, "pressure": 1013.25, "has_led": true} |
| BME280 | {"temperature": 24.35, "pressure": 1013.25, "humidity": 52.10, "has_led": true} |
Temperature in °C, pressure in hPa, humidity in %. The has_led field indicates whether the firmware was compiled with LED control enabled (-EnableLed). The monitor dashboard auto-detects the sensor type from the payload.
Kit default: LED is enabled. The kit includes a standard LED + 220Ω resistor wired to GPIO2 (active HIGH). See LED Wiring for the circuit diagram.
Before starting, make sure you have:
- Completed §1–§7 of the hardware setup guide — board, sensor, and Arduino IDE working
- Azure infrastructure deployed — run
terraform apply(terraform/README.md) - Azure CLI with
azure-iotextension installed (az extension add --name azure-iot)
Open Arduino IDE → Sketch → Include Library → Manage Libraries... and install:
| Library | Author | Required for |
|---|---|---|
| azure-sdk-for-c | Microsoft | Azure IoT Hub MQTT + SAS auth |
| Adafruit BMP280 | Adafruit | BMP280 sensor (install all dependencies) |
| Adafruit BME280 | Adafruit | BME280 sensor (only if you have one) |
From the firmware/ directory, run:
$pass = Read-Host "WiFi password" -AsSecureString
.\configure-sketch.ps1 -WifiSsid "YOUR_SSID" -WifiPassword $pass -SensorType BMP280 -EnableLed
⚠️ ReplaceYOUR_SSIDwith your actual Wi-Fi network name. The Wi-Fi must be 2.4 GHz — ESP32-C3 doesn't support 5 GHz networks.
What you'll see:
Reading Terraform outputs...
Reading device key from Azure CLI...
Generating sketch for BMP280...
✅ Sketch written to output\GlobalAzureKit\GlobalAzureKit.ino
❌ "terraform: not recognized"? You need to run this from the
firmware/directory, andterraform applymust have completed first. ❌ "Cannot find device"? Make sureterraform applyran successfully in Step 1.
The script automatically:
- Reads IoT Hub hostname and device ID from Terraform outputs
- Retrieves the device key from Azure CLI
- Renders the Mustache template with real values (via Poshstache)
- Selects the correct sensor code (BMP280, BME280, or auto-detect)
- Writes the ready-to-flash sketch to
firmware/output/GlobalAzureKit/
| Parameter | Required | Default | Values |
|---|---|---|---|
-WifiSsid |
Yes | — | Your WiFi network name |
-WifiPassword |
Yes | — | Your WiFi password |
-SensorType |
No | auto |
BMP280, BME280, auto |
-EnableLed |
No | (off) | Switch — enables LED control via C2D commands (GPIO2) |
-DeviceIndex |
No | 0 |
Device number (0, 1, 2...) |
Tip: use
-SensorType autoif you want the firmware to detect both BMP280 and BME280 at startup. Add-EnableLedto include Cloud-to-Device LED control (GPIO2). The telemetry will include"has_led": trueand the dashboard will show LED controls and state.
The kit includes a standard LED and a 220Ω resistor. Wire them before flashing with -EnableLed:
| ESP32-C3 | Component | Notes |
|---|---|---|
| GPIO2 | 220Ω resistor → LED anode (+) | Long leg |
| GND | LED cathode (−) | Short leg |
See esp32c3_super_mini_pinout.md for the full wiring diagram.
-
Open
firmware/output/GlobalAzureKit/GlobalAzureKit.inoin Arduino IDE -
Verify board settings in Tools menu:
Setting Value Board ESP32C3 Dev Module Port COMx (your port from §2) USB CDC On Boot Enabled -
Close Serial Monitor if open (can't use the port simultaneously)
-
Press Upload (→) — wait for
Done uploading. -
Open Tools → Serial Monitor — set baud rate to 115200
========================================
ExtraNet.GlobalAzureKit
Global Azure Torino 2026
========================================
2026/06/14 10:30:00 [INFO] BMP280 sensor detected at 0x76
2026/06/14 10:30:00 [INFO] Connecting to WiFi SSID MyNetwork
.....
2026/06/14 10:30:02 [INFO] WiFi connected, IP address: 192.168.1.42
2026/06/14 10:30:02 [INFO] Setting time using SNTP
.......
2026/06/14 10:30:05 [INFO] Time initialized!
2026/06/14 10:30:05 [INFO] Client ID: globalazure-kit-device-0
2026/06/14 10:30:05 [INFO] Username: globalazure-kit-iothub.azure-devices.net/...
2026/06/14 10:30:05 [INFO] MQTT client started
2026/06/14 10:30:06 [INFO] MQTT event MQTT_EVENT_CONNECTED
2026/06/14 10:30:06 [INFO] Subscribed for cloud-to-device messages; id:1
2026/06/14 10:30:06 [INFO] Sending telemetry ...
2026/06/14 10:30:06 [INFO] Message published: {"temperature":24.35,"pressure":1013.25,"has_led":true}
2026/06/14 10:30:06 [INFO] MQTT event MQTT_EVENT_PUBLISHED
If you see MQTT_EVENT_CONNECTED and Message published — the device is sending telemetry to Azure IoT Hub!
Check that messages are arriving at IoT Hub:
# Get your hub and device names from Terraform
cd ../terraform
$hubName = terraform output -raw iothub_name
$deviceId = terraform output -raw project_prefix
$deviceId = "${deviceId}-device-0"
az iot hub monitor-events --hub-name $hubName --device-id $deviceIdIf you used default settings, the values are
globalazure-kit-iothubandglobalazure-kit-device-0.Note: this command requires Azure CLI ≥ 2.67.0 and the
azure-iotextension.
You should see JSON payloads arriving in real-time. Press Ctrl+C to stop.
Default: 10 seconds. Configurable in the template:
#define TELEMETRY_FREQUENCY_MILLISECS 10000 // 10 secondsIoT Hub F1 free tier allows 8,000 messages/day (~1 every 11 seconds). For a workshop of a few hours, 10 seconds is safe.
The template .ino in firmware/GlobalAzureKit/ uses two mechanisms:
#define IOT_CONFIG_WIFI_SSID "{{WIFI_SSID}}"
#define IOT_CONFIG_IOTHUB_FQDN "{{IOTHUB_FQDN}}"Replaced with real values by configure-sketch.ps1.
// {{#BMP}}
#include <Adafruit_BMP280.h>
// {{/BMP}}
// {{#BME}}
#include <Adafruit_BME280.h>
// {{/BME}}| Tag | Kept when -SensorType is |
|---|---|
BMP |
BMP280 or auto |
BME |
BME280 or auto |
AUTO |
auto only |
ONLY_BMP |
BMP280 only |
ONLY_BME |
BME280 only |
LED |
-EnableLed switch is set |
Azure IoT Hub uses Microsoft PKI (issuer: MSFT RS256 CA-1, root: Microsoft RSA Root Certificate Authority 2017).
The azure_ca.h header shipped with azure-sdk-for-c (last updated Feb 2023) contains only:
- Baltimore CyberTrust Root — expired October 2025
- DigiCert Global Root G2 — valid, but not the primary root for Azure IoT Hub
It does not include Microsoft RSA Root Certificate Authority 2017, so using azure_ca.h directly causes TLS handshake failures.
The firmware bypasses azure_ca.h entirely and embeds both required roots directly in the sketch:
- Microsoft RSA Root Certificate Authority 2017 — primary, expires 2042
- DigiCert Global Root G2 — fallback, expires 2038
No action needed — the certificates are already embedded in the generated sketch.
| Problem | Solution |
|---|---|
| Serial Monitor empty | Tools → USB CDC On Boot → Enabled, then re-upload |
| "No BMP280/BME280 found" | Check wiring: SDA→GPIO8, SCL→GPIO9, VCC→3.3V. Try 0x77 instead of 0x76 |
| WiFi won't connect | Verify SSID/password. ESP32-C3 supports 2.4 GHz only (not 5 GHz) |
| MQTT_EVENT_ERROR (TLS) | Check Serial Monitor for esp_tls_last_esp_err codes. Ensure NTP time synced |
| MQTT_EVENT_ERROR (refused) | Verify device exists: az iot hub device-identity show --hub-name ... --device-id ... |
| "Failed generating SAS token" | Check IOT_CONFIG_DEVICE_KEY — must be a valid base64 key |
| Upload fails | Close Serial Monitor before uploading. Verify board/port in Tools |
configure-sketch.ps1 fails |
Run from firmware/ directory. Ensure terraform apply was run first |
firmware/
├── README.md # This file
├── configure-sketch.ps1 # Mustache template → configured sketch
├── GlobalAzureKit/
│ └── GlobalAzureKit.mustache # Mustache template (sensor blocks + credentials)
└── output/ # Generated sketches (gitignored)
└── GlobalAzureKit/
└── GlobalAzureKit.ino # Ready-to-flash sketch
The firmware is sending data — now start the dashboard to visualize it!
First, generate the .env file (if you haven't already):
cd ../terraform
terraform output -raw dot_env_content | Out-File -FilePath ../monitor/.env -Encoding UTF8Then start the dashboard:
cd ../monitor
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python app.pyOpen http://localhost:5000 and see your sensor data in real-time charts.
Full setup and API reference: monitor/README.md
Engineered by ExtraNet, AI-accelerated with GitHub Copilot
Global Azure Torino 2026