A large-scale IoT device simulator capable of running thousands of concurrent virtual devices that generate realistic telemetry and publish it over configurable protocols.
The system is composed of three services:
- Device Runtime (Go) β the data plane. Runs one goroutine per device, generates telemetry, and streams it over the configured protocol adapter.
- Simulation Orchestrator (Python) β the control plane. Loads device profiles, manages the fleet lifecycle, and exposes both a CLI and a REST API.
- Frontend (React + TypeScript) β a web dashboard for controlling the fleet and observing live telemetry via the orchestrator REST API.
Status: All phases complete. See docs/SYSTEM.md for the full design document.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Operator / CI β
β β
β iot-sim spawn / stop / status / stream REST API :8000 β
β iot-sim scenario run ramp_up.py POST /api/v1/devices/... β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β gRPC (SpawnDevices, StopDevices, β HTTP / SSE
β InjectFault, StreamTelemetry β¦) β
ββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββ
β Simulation Orchestrator (Python) β
β β
β βββββββββββββββ ββββββββββββββββ βββββββββββββββββ βββββββββββββββ β
β β Typer CLI β β FastAPI app β β RuntimePool β β Scenario β β
β β (cli.py) β β (api.py) β β (pool.py) β β Engine β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ βββββββββ¬ββββββββ β (scenario β β
β ββββββββββββββββββ΄βββββββββββββββββββΊβ β .py) β β
β β βββββββββββββββ β
β consistent-hash ring β
ββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β gRPC :50051
ββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ
β Device Runtime (Go) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Manager β β
β β spawnOne() βββΊ VirtualDevice goroutine Γ N β β
β β βββββββββββββββββββββββββββββββ β β
β β β ticker β Generator.Next() β β β
β β β applyFaults() β β β
β β β Publisher.Publish() β β β
β β ββββββββββββββββ¬βββββββββββββββ β β
β β β fan-in chan β β
β β Broadcaster ββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β MQTT pool β β HTTP POST β β AMQP chan β β protocol β
β β (paho) β β (net/http) β β pool β adapters β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β β
β :8080 /metrics (Prometheus) /healthz /readyz β
βββββββββββΌββββββββββββββββββΌββββββββββββββββββΌββββββββββββββββββββββββββ-ββ
β β β
βββββββββββΌββββββ βββββββββΌβββββββ ββββββββΌββββββββ βββββββββββββββ
β Mosquitto β β HTTP target β β RabbitMQ β β Prometheus β
β :1883 β β (any) β β :5672 β β :9090 β
βββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββ¬βββββββ
β
ββββββββΌβββββββ
β Grafana β
β :3000 β
βββββββββββββββ
Data generators (per telemetry field, seeded deterministically):
gaussian Β· static Β· brownian (Ornstein-Uhlenbeck) Β· diurnal (sinusoidal) Β· markov (state machine)
Fault injection (applied per device tick):
DISCONNECT Β· LATENCY_SPIKE Β· DATA_CORRUPTION Β· BATTERY_DRAIN Β· CLOCK_DRIFT
flowchart TB
subgraph Operator["Operator / CI"]
CLI["iot-sim CLI\nspawn Β· stop Β· status Β· stream\nscenario run"]
RESTC["REST client\ncurl / SDK"]
end
subgraph Orchestrator["Simulation Orchestrator (Python)"]
direction TB
API["FastAPI\n:8000"]
CLIAPP["Typer CLI"]
POOL["RuntimePool\nconsistent-hash ring"]
SCENARIO["ScenarioEngine\nSimClock Β· ScenarioContext"]
CLI --> CLIAPP
RESTC --> API
API --> POOL
CLIAPP --> POOL
SCENARIO --> POOL
end
subgraph Runtime["Device Runtime (Go) :50051"]
direction TB
GRPC["gRPC Server\nDeviceRuntimeService"]
MGR["Manager\nspawnOne Β· Stop Β· InjectFault"]
BC["Broadcaster\nfan-in channel"]
subgraph Devices["VirtualDevice goroutine Γ N"]
GEN["Generator.Next()\ngaussian Β· brownian\ndiurnal Β· markov Β· static"]
FAULT["applyFaults()\nDISCONNECT Β· LATENCY_SPIKE\nDATA_CORRUPTION Β· CLOCK_DRIFT"]
PUB["Publisher.Publish()"]
GEN --> FAULT --> PUB
end
GRPC --> MGR --> Devices
Devices --> BC
end
subgraph Frontend["Frontend (React) :3001"]
UI["Dashboard Β· Devices Β· Telemetry\nnginx β proxy /api"]
end
subgraph Infra["Infrastructure"]
MQTT["Mosquitto\n:1883"]
PROM["Prometheus\n:9090"]
GRAF["Grafana\n:3000"]
RABBIT["RabbitMQ\n:5672"]
HTTP["HTTP target\n(any)"]
end
UI -->|"REST / SSE"| API
POOL -->|gRPC| GRPC
PUB -->|MQTT| MQTT
PUB -->|AMQP| RABBIT
PUB -->|HTTP POST| HTTP
Runtime -->|"/metrics :8080"| PROM
PROM --> GRAF
.
βββ device-runtime/ # Go gRPC server + virtual device engine
β βββ cmd/runtime/ # Binary entry point
β βββ internal/
β βββ device/ # VirtualDevice, Manager, RuntimeClock
β βββ generator/ # Gaussian, Static generators + factory
β βββ protocol/ # Publisher interface + Console adapter
β βββ server/ # gRPC handlers, Broadcaster, interceptors
βββ orchestrator/ # Python orchestrator
β βββ orchestrator/
β β βββ api.py # FastAPI REST app
β β βββ cli.py # Typer CLI (iot-sim)
β β βββ config.py # Profile loader + Pydantic validation
β β βββ grpc_client.py # Typed async gRPC client
β βββ tests/
β βββ Pipfile # Pipenv dependency manifest
β βββ pyproject.toml # Package metadata + entry points
βββ frontend/ # React + TypeScript web dashboard
β βββ src/
β β βββ api/ # TanStack Query hooks + fetch client
β β βββ components/ # Shared MUI layout components
β β βββ pages/ # Dashboard, Devices, Telemetry pages
β βββ package.json
β βββ vite.config.ts # Dev server with /api proxy to :8000
βββ proto/simulator/v1/ # Protobuf definitions (source of truth)
βββ profiles/ # Device profile YAML files
βββ deployments/ # Docker Compose, Dockerfiles, nginx config
βββ docs/SYSTEM.md # Full technical design document
βββ IMPLEMENTATION_PLAN.md # Phase-by-phase implementation plan
βββ buf.yaml # Buf lint/breaking-change config
βββ Makefile # Build, test, and code-gen targets
| Tool | Version | Purpose |
|---|---|---|
| Docker + Compose | β₯ 24 | Run the full stack |
| Go | β₯ 1.21 | Device runtime (local dev) |
| Python | β₯ 3.12 | Orchestrator (local dev) |
| Node.js | β₯ 20 | Frontend (local dev) |
| pipenv | latest | Python dependency management |
| buf | latest | Protobuf linting and code generation |
The entire stack β runtime, orchestrator, frontend, MQTT broker, Prometheus, and Grafana β starts with a single command:
docker compose -f deployments/docker-compose.yaml up --build| Service | URL | Description |
|---|---|---|
| Frontend | http://localhost:3001 | React dashboard |
| Orchestrator API | http://localhost:8000 | FastAPI + Swagger UI at /docs |
| Runtime admin | http://localhost:8080 | /healthz Β· /readyz Β· /metrics |
| Grafana | http://localhost:3000 | Dashboards (admin / admin) |
| Prometheus | http://localhost:9090 | Metrics explorer |
Startup order is enforced via healthcheck dependencies:
mosquitto β runtime β orchestrator β frontend
make proto-genThis runs buf generate for Go and grpc_tools.protoc for Python, writing generated files to device-runtime/gen/go/ and orchestrator/gen/python/.
make go-build
./device-runtime/runtime --port 50051 --admin-port 8080 --log-level infoThe runtime exposes:
:50051β gRPC (DeviceRuntimeService):8080β Admin HTTP (/healthz,/readyz)
cd orchestrator
pipenv installIf you have Node.js β₯ 20 installed locally:
cd frontend
npm installIf Node.js is not installed locally, use Docker to install packages or generate the lock file:
# Generate / update package-lock.json
docker run --rm -v "$(pwd)/frontend:/app" -w /app node:20-alpine npm install
# Install packages only (no node_modules on host, lock file only)
docker run --rm -v "$(pwd)/frontend:/app" -w /app node:20-alpine npm install --package-lock-onlyThis is also required when adding new packages so that package-lock.json stays in sync before running make docker-build.
# Spawn 5 temperature sensors
pipenv run iot-sim spawn --profile ../profiles/temperature_sensor.yaml --count 5
# Check fleet status
pipenv run iot-sim status
# Stream live telemetry to the terminal
pipenv run iot-sim stream --type temperature_sensor
# Stop all devices
pipenv run iot-sim stop --all# Start the API server (default: http://localhost:8000)
pipenv run iot-sim serve
# Spawn devices
curl -X POST http://localhost:8000/api/v1/devices/spawn \
-H "Content-Type: application/json" \
-d '{"profile": "../profiles/temperature_sensor.yaml", "count": 5}'
# Fleet status
curl http://localhost:8000/api/v1/devices/status
# Stream telemetry (SSE)
curl -N "http://localhost:8000/api/v1/devices/stream?device_type=temperature_sensor"
# Stop all
curl -X POST http://localhost:8000/api/v1/devices/stop \
-H "Content-Type: application/json" \
-d '{"all": true}'Interactive API docs (Swagger UI) are available at http://localhost:8000/docs.
Profiles are YAML files in profiles/ that define a device type's telemetry schema, protocol, and generator configuration. See docs/DEVICE_PROFILES.md for the full reference.
# Go
make go-test
# Python
cd orchestrator && pipenv run pytest tests/ -v| Target | Description |
|---|---|
make proto-gen |
Generate Go + Python code from .proto files |
make proto-lint |
Lint proto files with Buf |
make proto-breaking |
Check for breaking changes against main |
make go-build |
Build the runtime binary |
make go-test |
Run Go tests |
make py-test |
Run Python tests |
make all |
Lint β generate β build β test (all) |
- docs/SYSTEM.md β full technical design: architecture, data models, concurrency model, API reference, and implementation status per phase.
- docs/DEVICE_PROFILES.md β device profile YAML reference with all fields and generator types.