Open-source IoT workshop kit for Global Azure Torino 2026. Stream real-time sensor telemetry from an ESP32-C3 to Azure IoT Hub and visualize it in a live dashboard — all deployed with one Terraform command.
Engineered by ExtraNet, built in collaboration with GitHub Copilot — code, infrastructure and documentation crafted through human-AI pair programming.
This isn't a branded gadget you forget in a drawer.
ExtraNet.GlobalAzureKit is a complete, working IoT pipeline — from a physical sensor on your desk to a real-time dashboard powered by Azure. We built it because we believe the best way to learn cloud isn't reading slides — it's getting your hands on real infrastructure, real telemetry, and real architecture decisions.
The kit reflects how we work every day: Terraform for repeatable deploys, Event Hub for scalable ingestion, Log Analytics for observability, least-privilege auth everywhere. The same patterns you see here are the ones that run in production — scaled up, hardened, and monitored.
GitHub Copilot was our accelerator: we directed the architecture, made the design choices, and Copilot turned them into code at speed. The expertise is ours; the velocity is AI.
| Component | Description |
|---|---|
| ESP32-C3 Super Mini | RISC-V single-core microcontroller, Wi-Fi 4, USB-C, I²C |
| BMP280 sensor | Temperature + atmospheric pressure (I²C, Bosch Sensortec) |
| BME280 support | Temperature + pressure + humidity — pin-compatible drop-in replacement |
| LED | Standard LED (any color) — controlled via Cloud-to-Device commands from the dashboard |
| 220Ω resistor | Current-limiting resistor for the LED (GPIO2 → 220Ω → LED → GND) |
| Arduino firmware | Template-based .ino with Azure SDK for Embedded C, SAS auth, MQTT/TLS |
| configure-sketch.ps1 | Auto-generates the firmware from Terraform outputs — zero manual credential copy |
| Terraform IaC | One-command deploy: IoT Hub, Event Hub, Log Analytics, monitoring, alerts |
| Monitor dashboard | Flask + Chart.js + Socket.IO — dark theme, real-time charts, CSV export, LED control |
| Workshop guides | Step-by-step ESP32-C3 wiring, flashing, and troubleshooting (§1–§8) |
graph TB
subgraph EDGE["🔌 Edge — ESP32-C3 + Sensors"]
ESP["ESP32-C3 Super Mini\nRISC-V · Wi-Fi 4 · USB-C"]
SENSOR["BMP280 / BME280\nTemperature · Pressure · Humidity*"]
LED["LED + 220Ω Resistor\nGPIO2 · Active HIGH"]
ESP -->|"I²C — SDA=GPIO8 · SCL=GPIO9"| SENSOR
ESP -->|"GPIO2"| LED
end
subgraph AZURE["☁️ Microsoft Azure"]
subgraph DM["📡 Device Management"]
IOT["IoT Hub · F1 Free Tier\nMQTT over TLS 1.2"]
end
subgraph SP["📊 Stream Processing"]
EH["Event Hub · Standard\n2 partitions · 1-day retention"]
CG["Consumer Group\nmonitor"]
end
subgraph OBS["🔍 Observability"]
LA["Log Analytics\n30-day retention"]
ALERT["Metric Alert\ndropped telemetry > 10"]
end
subgraph IAC["🏗️ Infrastructure as Code"]
TF["Terraform\nazurerm ~> 4.14"]
STATE["Remote State\nAzure Storage"]
end
end
subgraph LOCAL["🖥️ Local Dashboard"]
DASH["Flask · Chart.js · Socket.IO\nReal-time visualization"]
end
IOT -->|"C2D · LED commands"| ESP
ESP ==>|"D2C · MQTT/TLS"| IOT
IOT -->|"Message Route"| EH
EH --> CG -->|"AMQP"| DASH
IOT -.->|"Diagnostics"| LA
EH -.->|"Diagnostics"| LA
LA -.-> ALERT
TF -.->|"manages"| IOT
TF -.->|"manages"| EH
TF -.->|"manages"| LA
TF -->|"state"| STATE
style AZURE fill:#e8f1fb,stroke:#0078d4,stroke-width:3px
style EDGE fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
style LOCAL fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
* Humidity available only with BME280 sensor
| # | Step | Component | Protocol / Detail |
|---|---|---|---|
| 1 | Sense | BMP280 / BME280 | I²C @ 0x76 — temperature, pressure, humidity* |
| 2 | Serialize | ESP32-C3 firmware | {"temperature": 22.5, "pressure": 1013.2, "humidity": 65.3} |
| 3 | Transmit | ESP32-C3 Wi-Fi | MQTT over TLS 1.2 → IoT Hub |
| 4 | Authenticate | Azure IoT Hub | Per-device SAS token (connection string) |
| 5 | Route | IoT Hub Message Route | All D2C messages → Event Hub endpoint |
| 6 | Buffer | Event Hub | Standard, 2 partitions, 1-day retention |
| 7 | Consume | Monitor dashboard | AMQP via monitor consumer group |
| 8 | Visualize | Browser | Chart.js + WebSocket push via Socket.IO |
| 9 | Control | Dashboard → IoT Hub | C2D messages — LED on/off via {"action":"led","seconds":N} |
| 10 | Observe | Log Analytics | Diagnostic logs → KQL queries + metric alerts |
Goal: from zero to live dashboard in ~30 minutes.
Platform: the wizards and
start.battarget Windows + PowerShell. On Linux/macOS you can still run the manual steps (Option B) — Terraform, Azure CLI and Python are all cross-platform — but the guided experience is Windows-first.
Windows — double-click start.bat (or run .\wizard-gui.ps1 in PowerShell).
A graphical window opens and guides you through every step with buttons, text fields, and a live log:
- ✅ Checks prerequisites (Terraform, Azure CLI, Python) with download links
- 🔐 Login to Azure (one click)
- ☁️ Deploy ~12 Azure resources (one click + confirmation)
- 📡 Configure firmware (type WiFi SSID/password, choose sensor, click Generate)
- 📊 Launch the monitor dashboard (one click → opens browser)
Every step enables the next. You can close and re-run anytime — it skips what's already done.
Terminal user? Run
.\wizard.ps1instead for a CLI-based wizard with the same steps.
If you prefer to run each command yourself, expand the section below.
Every command shows what you should see. If the output doesn't match, a troubleshooting tip is right below.
Click to expand the manual procedure
| Step | What you'll do | Time |
|---|---|---|
| 1 | Deploy Azure infrastructure (IoT Hub, Event Hub, monitoring) | ~5 min |
| 2 | Wire ESP32-C3 + BMP280 sensor + LED with resistor and test with Arduino IDE | ~15 min |
| 3 | Flash the IoT Hub firmware (credentials auto-injected) | ~5 min |
| 4 | Launch the real-time dashboard and see live data | ~3 min |
Install these tools before starting. Click the link to download, then verify with the command:
| # | Tool | Install | Verify command |
|---|---|---|---|
| 1 | Terraform ≥ 1.5 | Download | terraform -version |
| 2 | Azure CLI ≥ 2.50 | Download | az version |
| 3 | Python ≥ 3.10 | Download | python --version |
| 4 | Arduino IDE 2.x | Download | Open the app |
| 5 | Azure subscription | Create free account | — |
⚠️ IoT Hub Free (F1) limit: Azure allows only one F1 IoT Hub per subscription. If you already have one (even empty or from an old experiment), the deploy will fail withMaxNumberOfIotHubsExceeded. Fix: delete the existing F1 hub, or setiothub_sku = "S1"interraform.auto.tfvars(~€21/month pro-rata — pennies for a workshop).
Open PowerShell and verify everything is installed:
terraform -version✅ You should see
Terraform v1.x.x. If it says "not recognized" → install Terraform and restart PowerShell.
az version✅ You should see
"azure-cli": "2.x.x". If it says "not recognized" → install Azure CLI and restart PowerShell.
python --version✅ You should see
Python 3.1x.x. If it says "not recognized" → install Python (check "Add to PATH" during setup!) and restart PowerShell.
Finally, install the Azure IoT extension (safe to re-run if already installed):
az extension add --name azure-iotOpen PowerShell in the repository folder (where you cloned/unzipped this project).
az loginWhat happens: your default browser opens. Sign in with your Azure account.
When done, the terminal shows your subscription info:
[
{
"name": "Your Subscription Name",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
...
}
]❌ Browser doesn't open? Try
az login --use-device-codeinstead — it gives you a code to enter at https://microsoft.com/devicelogin
cd terraformNo output? That's normal — it just changed directory.
.\scripts\setup-tfstate-backend.ps1What you'll see: the script creates an Azure Storage Account (takes ~1 minute):
Creating resource group 'globalazure-tfstate-rg'...
Creating storage account '...'...
Creating container 'tfstate'...
✅ Backend configuration written to backend.tfvars
❌ Error "subscription not found"? Go back to step 1a and make sure
az loginsucceeded.
Copy-Item terraform.auto.tfvars.example terraform.auto.tfvarsNo output? That's correct — the file was copied silently.
Now find your Subscription ID:
az account show --query id -o tsvWhat you'll see: a string like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Select it and copy (Ctrl+C).
Open the variables file in Notepad:
notepad terraform.auto.tfvarsFind this line:
subscription_id = "ENTER-YOUR-SUBSCRIPTION-ID-HERE"Replace the placeholder with your Subscription ID so it looks like:
subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"Save the file (Ctrl+S) and close Notepad.
terraform init -backend-config=backend.tfvarsWhat you'll see (takes a few seconds):
Initializing the backend...
...
Terraform has been successfully initialized!
❌ "Error configuring the backend"? Check that
backend.tfvarsexists (step 1c). ❌ "Backend configuration changed"? Typeyesto migrate.
terraform applyWhat you'll see: Terraform shows a plan listing ~12 Azure resources. At the bottom:
Do you want to perform these actions?
Enter a value:
Type yes and press Enter.
Wait 2–3 minutes. When it finishes:
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
🎉 Azure infrastructure is ready!
❌ Error about IoT Hub region? Open
terraform.auto.tfvarsagain and changeiothub_location = "westeurope"to"northeurope".Full details and troubleshooting: terraform/README.md
Follow the hardware setup guide (§1–§7):
| Guide | When to use |
|---|---|
| esp32c3_quickstart.md | Happy path — everything works first try |
| esp32c3_setup_guide.md | Troubleshooting — detailed explanations for each step |
| esp32c3_super_mini_pinout.md | Pinout reference — wiring diagram |
✅ Done when you see sensor readings in the Serial Monitor:
Temp: 24.35 C Pressure: 1013.25 hPa
In Arduino IDE: Sketch → Include Library → Manage Libraries...
Search and install these (one at a time):
| Search for | Click "Install" on | Notes |
|---|---|---|
azure-sdk-for-c |
azure-sdk-for-c by Microsoft | Required |
Adafruit BMP280 |
Adafruit BMP280 Library by Adafruit | Install if you have a BMP280 |
Adafruit BME280 |
Adafruit BME280 Library by Adafruit | Install if you have a BME280 |
Go back to PowerShell (you should still be in the terraform folder from Step 1):
cd ../firmware$pass = Read-Host "WiFi password" -AsSecureString
.\configure-sketch.ps1 -WifiSsid "YOUR_SSID" -WifiPassword $pass -SensorType BME280 -EnableLedReplace
BME280withBMP280if you have a BMP280 sensor.
⚠️ ReplaceYOUR_SSIDwith your Wi-Fi network name. You'll be prompted for the password securely. The Wi-Fi must be 2.4 GHz — ESP32-C3 doesn't support 5 GHz networks. Add-EnableLedto enable LED control from the dashboard (the kit includes an LED + 220Ω resistor on GPIO2).
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 moved to a different terminal. Open a new PowerShell in the repository folder and
cd firmware. ❌ "Cannot find device"? Step 1f (terraform apply) must have completed successfully first.
-
In Arduino IDE, open
firmware\output\GlobalAzureKit\GlobalAzureKit.ino -
Check Tools menu settings:
Setting Value Board ESP32C3 Dev Module Port COMx (the port you found in §2) USB CDC On Boot Enabled ⚠️ critical! -
Close the Serial Monitor if it's open (can't upload while it's using the port)
-
Press Upload (→ button) — wait for
Done uploading. -
Open Tools → Serial Monitor — set baud rate to 115200
What you'll see:
========================================
ExtraNet.GlobalAzureKit
Global Azure Torino 2026
========================================
[INFO] BMP280 sensor detected at 0x76
[INFO] Connecting to WiFi ...
.....
[INFO] WiFi connected, IP address: 192.168.x.x
[INFO] MQTT event MQTT_EVENT_CONNECTED
[INFO] Message published: {"temperature":24.35,"pressure":1013.25,"has_led":true}
If you see MQTT_EVENT_CONNECTED — the device is talking to Azure! 🎉
❌ Serial Monitor blank? → Tools → USB CDC On Boot → Enabled, then re-upload. ❌ "WiFi not connected"? → Check SSID/password. Must be 2.4 GHz (not 5 GHz). ❌ MQTT error? → Check error details in Serial Monitor. Try running
configure-sketch.ps1again.Full troubleshooting: firmware/README.md
Go back to PowerShell:
cd ../terraform
terraform output -raw dot_env_content | Out-File -FilePath ../monitor/.env -Encoding UTF8No output? That's correct — the
.envfile was created silently.
cd ../monitor
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtYou'll see pip downloading packages — wait until it finishes (takes ~30 seconds).
python app.pyWhat you'll see:
* Running on http://127.0.0.1:5000
❌ "EVENTHUB_CONNECTION_STRING not set"? → Step 4a failed. Go back and re-run the
terraform outputcommand. ❌ "No module named flask"? → Run.\.venv\Scripts\Activate.ps1first, thenpip install -r requirements.txt.
Open your browser and go to: http://localhost:5000
What you'll see:
- A dark-themed dashboard with the Azure color palette
- Temperature and pressure charts updating in real-time
- A device table showing your ESP32-C3 with its latest readings
- A telemetry log at the bottom with live messages
Congratulations — the kit is working end-to-end!
The complete data journey is live:
Sensor → ESP32-C3 → Wi-Fi → Azure IoT Hub → Event Hub → Dashboard
When you're done, remember to destroy the Azure resources to avoid ongoing costs:
cd terraform terraform destroy
| Sensor | Temperature | Pressure | Humidity | I²C Address | Notes |
|---|---|---|---|---|---|
| BMP280 | ✅ | ✅ | — | 0x76 / 0x77 | Included in the kit |
| BME280 | ✅ | ✅ | ✅ | 0x76 / 0x77 | Drop-in replacement, same pinout |
The dashboard auto-detects the sensor type from the telemetry payload:
- BMP280:
{"temperature": 22.5, "pressure": 1013.2, "has_led": true} - BME280:
{"temperature": 22.5, "pressure": 1013.2, "humidity": 65.3, "has_led": true}
When humidity data arrives, the humidity chart and stats appear automatically.
- Dark theme with Azure-inspired color palette
- 5 stat cards — active devices, messages, temperature, pressure, humidity
- Real-time charts — temperature, pressure, and humidity (Chart.js)
- Device filter pills — click to show/hide individual devices on charts and table
- Time range buttons — 1m, 5m, 15m, 1h, All
- CSV export — per device or all devices combined
- Device table — sensor type (BMP280/BME280), status, readings, message count
- LED indicator — shows LED status per device (grey/off/glowing) when firmware has LED enabled
- LED shortcut — one-click 💡 ON button for devices with LED support
- C2D messaging — send any JSON command to a device via Cloud-to-Device
- Collapsible telemetry log — color-coded entries with timestamps
- Auto-detection — humidity chart appears only when BME280 data arrives
- Live connection indicator — shows WebSocket status
See monitor/README.md for setup and API reference.
.
├── .gitignore # Git ignore rules
├── README.md # This file
├── start.bat # Double-click launcher for the GUI wizard
├── wizard-gui.ps1 # GUI setup wizard (Windows Forms)
├── wizard.ps1 # CLI setup wizard (terminal fallback)
├── LICENSE # Unlicense (public domain)
├── .editorconfig # Editor settings (indent, charset, EOL)
├── .github/
│ ├── copilot-instructions.md # Copilot rules and conventions
│ └── prompts/
│ └── terraform-deploy.prompt.md # Guided Azure deploy procedure
├── firmware/
│ ├── README.md # Firmware documentation
│ ├── 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
├── monitor/
│ ├── app.py # Flask server + Event Hub consumer
│ ├── requirements.txt # Python dependencies
│ ├── requirements-lock.txt # Pinned dependency versions
│ ├── README.md # Monitor documentation
│ └── templates/
│ └── index.html # Dashboard (Chart.js + Socket.IO)
├── terraform/
│ ├── main.tf # Provider, backend, locals
│ ├── variables.tf # Input variables
│ ├── resources.tf # IoT Hub, Event Hub, devices
│ ├── monitoring.tf # Log Analytics, diagnostics, alerts
│ ├── outputs.tf # Connection strings, .env output
│ ├── backend.tfvars.example # Backend config template
│ ├── terraform.auto.tfvars.example # Variables template
│ ├── .gitignore # Excludes tfstate and sensitive files
│ ├── README.md # Terraform documentation
│ └── scripts/
│ └── setup-tfstate-backend.ps1 # Bootstrap storage for tfstate
├── esp32c3_setup_guide.md # Full guide (§1–§8 + troubleshooting)
├── esp32c3_quickstart.md # Quick-start guide (happy path)
└── esp32c3_super_mini_pinout.md # ESP32-C3 pinout + wiring reference
This kit is designed for workshop environments. Credentials are never committed to git:
| Asset | Protection |
|---|---|
Terraform state (.tfstate) |
Remote Azure Storage backend, gitignored |
Variable files (.tfvars) |
.gitignore — only .example templates in repo |
| Event Hub connection string | .env file, gitignored |
| Device credentials | Mustache template with {{&token}} placeholders; configure-sketch.ps1 injects at runtime |
| Generated firmware | firmware/output/ is gitignored — never committed |
| Auth rules | Least privilege: send-only for devices, listen-only for monitor |
| Document | Description |
|---|---|
| terraform/README.md | Terraform infrastructure — deploy, customize, destroy |
| monitor/README.md | Monitor dashboard — setup, API reference, features |
| firmware/README.md | Arduino firmware — libraries, configure-sketch, upload, troubleshoot |
| esp32c3_setup_guide.md | Full ESP32-C3 guide: wiring, flashing, troubleshooting (§1–§8) |
| esp32c3_quickstart.md | Quick-start ESP32-C3 guide (happy path) |
| esp32c3_super_mini_pinout.md | ESP32-C3 Super Mini pinout + sensor wiring |
| Resource | Cost |
|---|---|
| IoT Hub F1 | Free (8,000 msgs/day, 500 devices) |
| Event Hub Standard | ~€13/month (1 TU) |
| Log Analytics | ~€2.50/GB ingested |
| Storage (tfstate) | ~€0.02/month |
| Estimated total | ~€13–18/month |
For a workshop lasting a few hours, the cost is negligible. Run
terraform destroywhen done!
This kit is designed for hands-on learning — not production use — but every architectural choice mirrors real-world practice:
| Decision | Why |
|---|---|
| Event Hub instead of built-in IoT Hub endpoint | Decouples ingestion from consumption — the same pattern used when you add Stream Analytics, Azure Functions, or multiple consumers in production |
| Terraform with remote state | Infrastructure as Code from day one — not portal clicks you can't reproduce. Remote state on Azure Storage enables team collaboration |
| Separate auth rules (send / listen) | Least-privilege by default. The device can only send; the dashboard can only read. A compromised consumer can't inject messages |
| Log Analytics + metric alerts | Observability isn't an afterthought — if telemetry drops, you know immediately. This is the same stack that monitors production workloads |
| Mustache-based firmware template | Credentials never touch git. The generation script reads secrets at runtime from Terraform + Azure CLI — zero manual copy-paste |
| SAS token auth (not X.509) | Simpler for a workshop, but the architecture supports certificate-based auth with minimal changes when you move to production |
| ExtraNet Srl | Architecture, design decisions, and project direction. We build cloud solutions on Azure — this kit is a taste of how we work |
| GitHub Copilot | AI-accelerated development — code, infrastructure, and documentation generated through human-AI collaboration |
| Global Azure | The worldwide community event this kit was created for |
| Bosch Sensortec | BMP280 / BME280 sensor hardware |
| Espressif Systems | ESP32-C3 RISC-V microcontroller |
This project is released into the public domain under the Unlicense. You can copy, modify, distribute, and use it for any purpose — no attribution required.
Engineered by ExtraNet, AI-accelerated with GitHub Copilot
Global Azure Torino 2026