MQTT-linked co-simulation engine for the ModelScript ecosystem. Enables real-time co-simulation of Modelica models and FMUs orchestrated via an ISA-95 Unified Namespace (UNS).
┌─────────────────────────────────────────────────────────┐
│ Orchestrator │
│ (Gauss-Seidel Master Algorithm) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ JS-Sim │ │ FMU-JS │ │FMU-Natv │ │External │ │
│ │Particip.│ │Particip.│ │Particip.│ │Particip.│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴──────────────┘ │
│ │ │
└────────────────────────────┼─────────────────────────────┘
│ MQTT (UNS)
┌───────────┼───────────┐
│ Eclipse Mosquitto │
│ :1883 (MQTT) │
│ :9001 (WebSocket) │
└───────────┬───────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────┴────┐ ┌─────┴────┐ ┌──────┴─────┐
│ Morsel │ │ VS Code │ │ Historian │
│ (Browser) │ │ IDE │ │(TimescaleDB)│
└──────────┘ └──────────┘ └────────────┘
All MQTT topics follow an ISA-95 hierarchy:
modelscript/site/{siteId}/area/{areaId}/line/{sessionId}/cell/{participantId}/...
| Topic Suffix | QoS | Retained | Description |
|---|---|---|---|
/participants/{id}/meta |
1 | ✓ | Birth/death certificate |
/line/{session}/control |
1 | ✗ | Orchestrator → participant commands |
/line/{session}/status |
0 | ✗ | Participant → orchestrator status |
/line/{session}/results |
0 | ✗ | Aggregated step results |
/line/{session}/cell/{id}/data/{var} |
0 | ✗ | Individual variable telemetry |
/line/{session}/cell/{id}/data/_batch |
0 | ✗ | Batched variable telemetry |
src/
├── mqtt/
│ ├── topics.ts # ISA-95 UNS topic builders and parsers
│ ├── protocol.ts # Message schemas (metadata, control, status, results)
│ └── client.ts # Typed MQTT client with LWT and discovery
├── participant.ts # CoSimParticipant interface (FMI-2.0-style)
├── coupling.ts # Variable coupling graph (output → input)
├── realtime.ts # Wall-clock pacer with speedup factor
├── session.ts # Session lifecycle + state machine + registry
├── orchestrator.ts # Gauss-Seidel master algorithm
├── participants/
│ ├── js-simulator.ts # JS-native participant (wraps ModelicaSimulator)
│ ├── fmu-js.ts # FMU-JS participant (loads FMU metadata from storage)
│ └── fmu-native.ts # FMU-native participant (subprocess via JSON-RPC)
├── fmu/
│ ├── model-description.ts # FMI 2.0 modelDescription.xml regex parser
│ └── storage.ts # FMU archive storage + ZIP extraction
├── historian/
│ ├── recorder.ts # MQTT → TimescaleDB batch recorder
│ └── replayer.ts # TimescaleDB → MQTT session replay
├── ws/
│ └── stream.ts # WebSocket streaming with throttling
└── index.ts # Public API exports
| Type | Runtime | Status | Description |
|---|---|---|---|
js-simulator |
In-process | ✅ Implemented | Wraps @modelscript/core ModelicaSimulator |
fmu-js |
In-process | ✅ Implemented | Loads FMU metadata from storage, I/O coupling |
fmu-native |
Subprocess | ✅ Implemented | JSON-RPC harness for native FMU .so/.dll |
external |
MQTT | ✅ Implemented | External device/simulator publishing via MQTT |
import {
CosimMqttClient,
CoSimSession,
Orchestrator,
JsSimulatorParticipant,
createUnsContext,
} from "@modelscript/cosim";
// 1. Connect to MQTT broker
const unsContext = createUnsContext("lab", "project-a");
const mqttClient = new CosimMqttClient({
brokerUrl: "mqtt://localhost:1883",
unsContext,
});
await mqttClient.connect();
// 2. Create a session
const session = new CoSimSession(
"sim-001",
{
startTime: 0,
stopTime: 10,
stepSize: 0.01,
},
1.0,
); // 1.0 = real-time factor
// 3. Add participants
const participant = new JsSimulatorParticipant({
id: "bouncingBall",
dae: flattenedDAE, // from @modelscript/core flattener
});
session.addParticipant(participant);
// 4. Configure couplings (for multi-participant scenarios)
session.addCoupling({
from: { participantId: "plant", variableName: "y" },
to: { participantId: "controller", variableName: "u" },
});
// 5. Run the orchestrator
const orchestrator = new Orchestrator(session, mqttClient, {
onStep: (result) => console.log(`t=${result.time}`),
onComplete: () => console.log("Done!"),
onError: (err) => console.error(err),
});
await orchestrator.run();// Subscribe to participant birth/death certificates
await mqttClient.subscribeParticipants();
mqttClient.onParticipant((id, meta) => {
if (meta) {
console.log(`Participant online: ${meta.modelName} (${meta.type})`);
console.log(` Variables: ${meta.variables.length}`);
} else {
console.log(`Participant offline: ${id}`);
}
});import { HistorianQuery } from "@modelscript/cosim";
import { Pool } from "pg";
const pool = new Pool({ connectionString: "postgresql://localhost:5432/modelscript" });
const historian = new HistorianQuery(pool);
// Raw telemetry
const raw = await historian.queryRaw("bouncingBall", "h", new Date("2024-01-01"), new Date("2024-01-02"));
// Downsampled (1-second buckets)
const downsampled = await historian.queryAggregated(
"bouncingBall",
"h",
new Date("2024-01-01"),
new Date("2024-01-02"),
1,
"avg",
);The co-simulation engine requires two additional Docker services (added to the root docker-compose.yml):
| Service | Image | Ports | Purpose |
|---|---|---|---|
mqtt |
Eclipse Mosquitto | 1883, 9001 | MQTT broker (TCP + WebSocket) |
timescaledb |
TimescaleDB | 5432 | Time-series historian database |
| Variable | Default | Description |
|---|---|---|
MQTT_BROKER_URL |
mqtt://localhost:1883 |
MQTT broker connection URL |
TIMESCALEDB_URL |
postgresql://modelscript:modelscript@localhost:5432/modelscript |
Database connection string |
COSIM_UNS_SITE |
default |
UNS site identifier |
COSIM_UNS_AREA |
default |
UNS area identifier |
# Build
npm run build --workspace=@modelscript/cosim
# Lint
npm run lint --workspace=@modelscript/cosim
# Watch mode
npm run watch --workspace=@modelscript/cosimAll phases complete. The co-simulation engine is fully implemented.
AGPL-3.0-or-later