Skip to content

brianbirir/virtual-iot-simulator

Repository files navigation

Virtual IoT Simulator

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.


System Overview

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚                        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
Loading

Repository Layout

.
β”œβ”€β”€ 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

Prerequisites

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

Quick Start

Docker (recommended)

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


Local Development

1. Generate protobuf code

make proto-gen

This runs buf generate for Go and grpc_tools.protoc for Python, writing generated files to device-runtime/gen/go/ and orchestrator/gen/python/.

2. Start the Device Runtime

make go-build
./device-runtime/runtime --port 50051 --admin-port 8080 --log-level info

The runtime exposes:

  • :50051 β€” gRPC (DeviceRuntimeService)
  • :8080 β€” Admin HTTP (/healthz, /readyz)

3. Install Python dependencies

cd orchestrator
pipenv install

4. Install Frontend dependencies

If you have Node.js β‰₯ 20 installed locally:

cd frontend
npm install

If 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-only

This is also required when adding new packages so that package-lock.json stays in sync before running make docker-build.

5. Control devices via CLI

# 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

6. Control devices via the REST API

# 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.


Device Profiles

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.


Development

Run tests

# Go
make go-test

# Python
cd orchestrator && pipenv run pytest tests/ -v

Makefile targets

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)

Documentation

  • 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.

About

Simulate IoT devices to test IoT ingestion platforms hence removing the need of actual devices. Good for quickening development in cases where devices are scarce to access

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors