██████╗ ██╗ ██████╗ ██╗████████╗ █████╗ ██╗ ████████╗██╗ ██╗██╗███╗ ██╗ ██╔══██╗██║██╔════╝ ██║╚══██╔══╝██╔══██╗██║ ╚══██╔══╝██║ ██║██║████╗ ██║ ██║ ██║██║██║ ███╗██║ ██║ ███████║██║ ██║ ██║ █╗ ██║██║██╔██╗ ██║ ██║ ██║██║██║ ██║██║ ██║ ██╔══██║██║ ██║ ██║███╗██║██║██║╚██╗██║ ██████╔╝██║╚██████╔╝██║ ██║ ██║ ██║███████╗ ██║ ╚███╔███╔╝██║██║ ╚████║ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝
🎯 A TypeScript framework for building event-driven digital-twin simulators of industrial systems — you register devices and an orchestrator as a plugin, the engine ticks them on a scaled clock, and telemetry streams over an internal bus to MQTT and a live dashboard. Ships a full quarry / aggregates-plant as the reference industry.
🏭 Standing up a realistic industrial data stream normally means wiring real PLCs, sensors, and a broker — or hand-faking JSON. This project is the middle path: describe your plant in a JSON config, get a running fleet of virtual devices that emit physically-plausible telemetry (motor RPM, vibration, energy, throughput), talk to each other over a routed event bus, and publish to MQTT — with a web dashboard to watch it live.
Map • Core • Architecture • Quick start • Build your own • Dashboard & MQTT • Status • Roadmap • License
Digital-Twin-Simulator/
├── src/
│ ├── index.ts # Library barrel — public API surface (re-exports)
│ ├── cli.ts # Runnable app / composition root (main, MQTT bridge, dashboard)
│ ├── factory.ts # createDevice() helper over the registry
│ ├── types.ts # Core config & domain types
│ ├── core/
│ │ ├── BaseDevice.ts # Device SDK: lifecycle, failure model, sim-time, throttled logs
│ │ ├── engine/ # SimulatorEngine (tick loop) + orchestrator templates
│ │ ├── registry/ # Device & Orchestrator registries (plugin targets)
│ │ └── plugins/ # PluginLoader (dynamic import + register)
│ ├── runtime/ # EventBus, EdgeRouter, SimClock, Scheduler, rate limiters
│ ├── comms/ # CommsAdapter interface + MqttAdapter (mqtts/AWS IoT)
│ ├── logging/ # Composable loggers (ndjson, pretty, per-device, story, console)
│ ├── dashboard/ # Embedded HTTP + SSE dashboard server
│ └── industries/
│ └── quarry/ # ⭐ Reference industry plugin (8 devices + orchestrator)
├── docs/ # Architecture, PRD, device specs, verification reports
├── example.config.quarry.json # ✅ Working config — runs the quarry plant
├── config.json # ⚠️ Legacy dairy config (schema only — no device impls yet)
├── tsconfig.json
└── package.json
| Marker | Meaning |
|---|---|
| ⭐ | The reference implementation — read this to learn the plugin pattern |
| ✅ | The config that actually runs today |
| Aspirational / legacy — see Status | |
dist/, node_modules/, logs/ |
Generated or installed — git-ignored, never committed |
Everything above the industries/ line is domain-agnostic. A new simulator is a plugin that registers devices + an orchestrator; the engine never imports your domain.
| Layer | Module(s) | Responsibility |
|---|---|---|
| Runtime | runtime/SimClock, runtime/Scheduler |
Scaled simulated clock; real-time tick driver |
| Bus | runtime/EventBus, runtime/EdgeRouter |
Pub/sub with * wildcard; device→device routing with loop-prevention + token-bucket rate limits |
| Engine | core/engine/SimulatorEngine |
Owns the ticker set; on each tick advances the clock and calls tick(nowMs) on every device/orchestrator |
| Device SDK | core/BaseDevice |
Lifecycle (start/tick/stop), failure-window model, running-hours, emit/handleInputEvent/handleCommand, throttled & banded logging, toSimulatedTime() |
| Coordination | core/engine/BaseOrchestrator, EntityOrchestrator |
Higher-level flow control (sessions, orders, batches) above individual devices |
| Extensibility | core/registry/*, core/plugins/PluginLoader |
String-keyed registries + dynamic-import plugin loader — how industries plug in |
| Transport | comms/CommsAdapter, comms/MqttAdapter |
Protocol seam; MQTT over TLS (incl. AWS IoT cert auth) |
| Observability | logging/*, dashboard/DashboardServer |
Composable NDJSON/pretty/per-device/story loggers; HTTP + SSE live dashboard |
Event-driven, tick-based. Config declares devices and wiring; the engine ticks them; devices emit onto the bus; the router forwards along declared edges; the orchestrator coordinates; a bus bridge relays to MQTT; loggers and the dashboard observe.
config (JSON) ──▶ loadConfig ──▶ validateConfig ──▶ PluginLoader ──registers──▶ Registries
│
creates devices +
orchestrator
│
┌──────────────────────────── SimulatorEngine (tick loop) ─────────────────────┴────────┐
│ SimClock (scale ×N) + Scheduler (setInterval) │
│ │
│ every tick ▶ for each ticker: tick(nowMs) │
│ │
│ Device ──emit──▶ EventBus ──▶ EdgeRouter ──route(edge)──▶ Device.handleInput │
│ ▲ │ │
│ │ handleCommand ├──▶ Orchestrator (session · orders · batch lifecycle) │
│ └── cmd.* ◀───────┤ │
│ ├──▶ MQTT bridge ──▶ MqttAdapter ──▶ broker (mqtts) │
│ └──▶ Loggers (ndjson · pretty · per-device · story · console)│
└─────────────────────────────────────────────────┬───────────────────────────────────────┘
│
DashboardServer (HTTP + SSE · :3000)
Key properties, verifiable in the source:
- Loop-safe routing —
EdgeRoutertags forwarded eventssource: 'router'; devices ignore non-deviceevents, so a wired graph can't feedback-loop (runtime/EventBus.ts). - Scale-correct timing — devices express windows in real-ms and convert with
toSimulatedTime(), so behavior is stable acrosstime.scale(core/BaseDevice.ts). - Silent / throttled publishing — the MQTT bridge supports per-device silent mode, per-event minimum publish intervals, and a global fair rate limiter (
cli.ts).
Requirements: Node.js ≥ 18 (developed on 22), npm.
git clone https://github.com/AkashVarma007/Digital-Twin-Simulator.git
cd Digital-Twin-Simulator
npm install
npm run build✅ Verify the build: dist/cli.js and dist/industries/quarry/index.js now exist. (npm run typecheck and npm run build both exit 0 — verified.)
Run the reference quarry simulation:
node dist/cli.js ./example.config.quarry.json✅ What you should see: a startup banner, per-device creation logs, then a live story of the plant — inbound trucks weighed, material fed → crushed → screened → conveyed → stockpiled → loaded out. Structured logs stream to ./logs/, and the dashboard comes up on http://localhost:3000.
⚠️ Do not run the barenode dist/cli.jswith no argument. It defaults toconfig.json, which is a legacy dairy config whose device types have no implementations yet — it will fail withUnknown device type. Always passexample.config.quarry.json. See Status.
The quarry is just a plugin. To model a different system, you write the same three pieces:
1. Devices — extend BaseDevice, implement onTick() (your physics) and optionally handleInputEvent() (react to upstream devices):
import { BaseDevice } from "digital-twin-simulator";
export class PumpDevice extends BaseDevice {
protected onTick(nowMs: number, dtSeconds: number): void {
const flow = 12 + (Math.random() - 0.5); // m³/h
this.emit("pump.flow", { flow_m3h: flow });
}
protected handleInputEvent(type: string, data: any): void {
if (type === "tank.level") { /* react to upstream */ }
}
}2. An orchestrator (optional) — implement TickerLike to coordinate sessions, orders, or batches across devices.
3. A plugin entry — register your types, then point a config at it:
// src/industries/water/index.ts
import { DeviceRegistry, OrchestratorRegistry } from "digital-twin-simulator";
import { PumpDevice } from "./devices/Pump.js";
export function register() {
DeviceRegistry.register("pump", PumpDevice);
// OrchestratorRegistry.register("water", WaterOrchestrator);
}Wire devices with connections edges; the EdgeRouter delivers each upstream emit to the downstream device's handleInputEvent. See src/industries/quarry/ end-to-end and docs/DEVICE_COMMUNICATION_PROTOCOL.md for the event contract.
Dashboard — an embedded zero-dependency HTTP server (src/dashboard/DashboardServer.ts) with Server-Sent-Events live updates:
| Route | Purpose |
|---|---|
/ |
Live device grid |
/device/{id} |
Per-device detail: metrics, recent logs, events |
/api/devices |
Device status JSON |
/api/devices/{id}/logs |
Recent device logs |
/api/stream |
SSE event stream |
/api/devices/export-md |
Device-state snapshot as Markdown |
MQTT — set a connection block on a device (protocol: "mqtt", endpoint, mqtt.topicBase) and the bus bridge publishes its events to topicBase/<event>. TLS is on by default; AWS IoT Core cert auth is supported via connection.awsIot (see config.aws-iot.example.json). Tokens/endpoints come from env / .env, never hardcoded.
| Command | What it does |
|---|---|
npm install |
Install dependencies |
npm run build |
Compile src/ → dist/ (also required before running any plugin config) |
npm run typecheck |
Type-check with no emit |
npm run dev |
Run the app from source via ts-node (src/cli.ts) |
npm start |
Run the compiled app (node dist/cli.js) |
Configuration reference and deeper design live in docs/ — start with docs/architecture.md, docs/DEVICE_COMMUNICATION_PROTOCOL.md, and docs/SIMULATION_SPEED.md.
Honest state of the project — this is an actively-evolving reference framework, not a released library.
- One working industry. The quarry plugin is complete and verified (see
docs/VERIFICATION_REPORT.md). It is the only industry with device implementations. - Dairy config is schema-only.
config.json/types.ts/config/schema.tscarry a dairy supply-chain taxonomy (Source → ProcessingPlant → ColdStorage → Market) with no device implementations — running it fails by design today. It is kept as the target for a second industry, not a working demo. - No automated tests. There is no test runner yet (the top-level
test_vehicle_plc.jsis a stale manual script and does not run). Build and type-checking are wired into CI; behavioral tests are on the roadmap. - Single-process scope. Registries and the sim clock are module singletons — one simulation per process. Fine for the CLI; not yet suitable for parallel/isolated runs.
- Real-time-locked clock. Ticks are driven by
setInterval;time.scaleis integer ≥ 1 (no sub-real-time or headless max-speed stepping yet). - Dependency advisories.
npm auditreports advisories from transitive dependencies (via thetask-master-aidev tooling); a dependency cleanup is planned. - Repo hygiene pending. Some legacy files (BMAD/
.cursorscaffolding, redundant example configs, dead device modules) are slated for removal in a follow-up cleanup.
- Extract the domain vocabulary out of core (
schema.ts/types.ts) so validation is plugin-owned. - Engine-instance scoping (drop global singletons) + a seedable RNG for reproducible runs.
- Decoupled time control: pause / step / fractional scale / headless max-speed.
- A test suite (device unit tests + a quarry end-to-end smoke test) feeding a real CI badge.
- A second, fully-implemented industry to prove the framework generalizes.
Contributions welcome. See CONTRIBUTING.md for setup, conventions, and how to add a device or industry, and CODE_OF_CONDUCT.md. Security issues: see SECURITY.md.
MIT © 2026 Akash Varma.