Skip to content

About

Event-driven TypeScript framework for building digital-twin simulators of industrial systems. Register devices + an orchestrator as a plugin; the engine ticks them on a scaled clock and streams telemetry over an event bus to MQTT and a live dashboard. Includes a full quarry/aggregates-plant reference industry.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

██████╗ ██╗ ██████╗ ██╗████████╗ █████╗ ██╗         ████████╗██╗    ██╗██╗███╗   ██╗
██╔══██╗██║██╔════╝ ██║╚══██╔══╝██╔══██╗██║         ╚══██╔══╝██║    ██║██║████╗  ██║
██║  ██║██║██║  ███╗██║   ██║   ███████║██║            ██║   ██║ █╗ ██║██║██╔██╗ ██║
██║  ██║██║██║   ██║██║   ██║   ██╔══██║██║            ██║   ██║███╗██║██║██║╚██╗██║
██████╔╝██║╚██████╔╝██║   ██║   ██║  ██║███████╗       ██║   ╚███╔███╔╝██║██║ ╚████║
╚═════╝ ╚═╝ ╚═════╝ ╚═╝   ╚═╝   ╚═╝  ╚═╝╚══════╝       ╚═╝    ╚══╝╚══╝ ╚═╝╚═╝  ╚═══╝

D I G I T A L   T W I N   S I M U L A T O R

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

License: MIT TypeScript 5.9 Node >=18 CI status MQTT Status: reference implementation

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


🗺️ Repository map

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

🧰 The framework core

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

🏛️ Architecture

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 — EdgeRouter tags forwarded events source: 'router'; devices ignore non-device events, 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 across time.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).

🚀 Quick start

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 bare node dist/cli.js with no argument. It defaults to config.json, which is a legacy dairy config whose device types have no implementations yet — it will fail with Unknown device type. Always pass example.config.quarry.json. See Status.

🧩 Build your own simulator

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);
}
// your.config.json
{
  "plugins": [{ "path": "./dist/industries/water/index.js" }],
  "orchestrator": { "id": "water" },
  "devices": [ { "identity": { "id": "pump-01", "type": "pump" } } ],
  "connections": [ { "from": "tank-01", "to": "pump-01" } ]
}

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 and MQTT

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.

🧑‍💻 Working with it

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.

🧭 Status and limitations

Honest state of the project — this is an actively-evolving reference framework, not a released library.

tests: none yet industries: quarry only

  • 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.ts carry 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.js is 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.scale is integer ≥ 1 (no sub-real-time or headless max-speed stepping yet).
  • Dependency advisories. npm audit reports advisories from transitive dependencies (via the task-master-ai dev tooling); a dependency cleanup is planned.
  • Repo hygiene pending. Some legacy files (BMAD/.cursor scaffolding, redundant example configs, dead device modules) are slated for removal in a follow-up cleanup.

🛣️ Roadmap

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

🤝 Contributing

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.

📄 License

MIT © 2026 Akash Varma.

Built with TypeScript · event-driven · plugin-based · a base for simulators of many kinds.

About

Event-driven TypeScript framework for building digital-twin simulators of industrial systems. Register devices + an orchestrator as a plugin; the engine ticks them on a scaled clock and streams telemetry over an event bus to MQTT and a live dashboard. Includes a full quarry/aggregates-plant reference industry.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages