mqtt-scenario-sim 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 dislev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="mqtt-scenario-sim logo" width="160" />
3
+ </p>
4
+
5
+ # mqtt-scenario-sim
6
+
7
+ [![CI](https://github.com/dislev/mqtt-scenario-sim/actions/workflows/ci.yml/badge.svg)](https://github.com/dislev/mqtt-scenario-sim/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/badge/npm-v1.0.0-blue)](https://www.npmjs.com/package/mqtt-scenario-sim)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
10
+ [![Node >=22](https://img.shields.io/badge/node-%3E%3D22-green)](package.json)
11
+ [![codecov](https://codecov.io/gh/dislev/mqtt-scenario-sim/branch/main/graph/badge.svg)](https://codecov.io/gh/dislev/mqtt-scenario-sim)
12
+
13
+ A configurable MQTT sensor simulator with built-in test scenarios. Publish synthetic sensor data over MQTT — as JSON or protobuf — without real hardware.
14
+
15
+ Define your sources with any labels you want. Wire up any MQTT topic template. Switch between test scenarios over HTTP to drive your alert and pipeline logic.
16
+
17
+ > **Maintenance:** This project is maintained on a best-effort basis. Bug reports and PRs are welcome — response times may vary.
18
+
19
+ ---
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ npx mqtt-scenario-sim --config examples/minimal.yaml
25
+ ```
26
+
27
+ Requires a running MQTT broker (e.g. `docker run -p 1883:1883 eclipse-mosquitto`).
28
+
29
+ ---
30
+
31
+ ## Scenarios
32
+
33
+ The real differentiator. Switch your entire simulator into a controlled test state at any time — no restarts.
34
+
35
+ | ID | Key | What it does |
36
+ |---|---|---|
37
+ | 0 | `normal` | Each metric runs its configured mode |
38
+ | 1 | `out_of_range` | All metrics 30% above max — tests breach detection |
39
+ | 2 | `trending_to_breach` | Rising toward max — tests early-warning alerts |
40
+ | 3 | `stable_healthy` | Held at midpoint ideal — tests steady-state |
41
+ | 4 | `recovery` | Starts out-of-range, decays to ideal over ~5 min |
42
+ | 5 | `oscillating` | ±10% swing — tests flapping suppression |
43
+
44
+ ```bash
45
+ # Activate a scenario
46
+ curl -X POST http://localhost:4000/scenario/1
47
+
48
+ # With auto-revert after 60 seconds
49
+ curl -X POST "http://localhost:4000/scenario/4?durationSeconds=60"
50
+
51
+ # Target a single source by its labels key
52
+ curl -X POST "http://localhost:4000/scenario/2?sourceKey=%7B%22device%22%3A%22node-1%22%7D"
53
+
54
+ # Check current scenario
55
+ curl http://localhost:4000/scenario
56
+
57
+ # Back to normal
58
+ curl -X POST http://localhost:4000/scenario/0
59
+ ```
60
+
61
+ ---
62
+
63
+ ## YAML config
64
+
65
+ ```yaml
66
+ mqtt:
67
+ host: localhost
68
+ port: 1883
69
+
70
+ publishIntervalMs: 5000
71
+
72
+ encoding:
73
+ type: json # or "protobuf" (see Protobuf section)
74
+
75
+ sources:
76
+ - labels: # fully freeform — any keys you want
77
+ building: hq
78
+ floor: "3"
79
+ zone: east
80
+ topic: "{building}/{floor}/{zone}/metrics" # template using label keys
81
+ metrics:
82
+ - name: temperature
83
+ units: "°C"
84
+ mode: sinusoidal # sinusoidal | drift | normal | spike
85
+ range: { low: 18, high: 28 }
86
+ periodSeconds: 3600 # optional
87
+ - name: humidity
88
+ units: "%"
89
+ mode: drift
90
+ range: { low: 30, high: 70 }
91
+ effects: # optional — external things that bias metric readings
92
+ - name: hvac
93
+ effects:
94
+ temperature: -0.40 # fraction of range span, negative = reduce
95
+ humidity: -0.20
96
+ ```
97
+
98
+ ### Sensor modes
99
+
100
+ | Mode | Description |
101
+ |---|---|
102
+ | `sinusoidal` | Smooth oscillation over `periodSeconds` |
103
+ | `normal` | Gaussian noise around baseline |
104
+ | `drift` | Slow random walk that bounces at range edges |
105
+ | `spike` | Normal noise with random anomaly spikes |
106
+
107
+ ### Effects
108
+
109
+ Effects let you simulate external influences on metric readings. Send a command to `{topic}/cmd`:
110
+
111
+ ```json
112
+ { "effect": "hvac", "state": true }
113
+ ```
114
+
115
+ Each effect's `effects` map specifies bias as a fraction of the metric's span. `-0.40` on temperature with a span of 10°C = -4°C bias.
116
+
117
+ ---
118
+
119
+ ## Protobuf
120
+
121
+ ```yaml
122
+ encoding:
123
+ type: protobuf
124
+ protoFile: ./my.proto
125
+ messageType: myapp.SensorReading
126
+ fieldMap: # optional — remap internal fields to proto field names
127
+ metric: sensor_name
128
+ value: reading_value
129
+ timestamp: recorded_at
130
+ ```
131
+
132
+ Internal fields available for mapping: `metric`, `value`, `units`, `timestamp`, plus any label key (e.g. `building`, `floor`).
133
+
134
+ ---
135
+
136
+ ## HTTP API
137
+
138
+ | Method | Path | Description |
139
+ |---|---|---|
140
+ | `GET` | `/stream` | SSE stream — one event per metric publish |
141
+ | `GET` | `/status` | Full snapshot: uptime, scenario, all readings, all effects |
142
+ | `GET` | `/health` | Status, uptime, source/metric counts, active scenario |
143
+ | `GET` | `/scenario` | Current scenario name + description |
144
+ | `POST` | `/scenario/:id` | Activate scenario (0–5 or name). Query: `durationSeconds`, `sourceKey` |
145
+ | `GET` | `/state` | Last published value per metric |
146
+ | `GET` | `/effects` | Current effect state per source |
147
+
148
+ ### Live tailing with `/stream`
149
+
150
+ `/stream` is a Server-Sent Events endpoint. It pushes one JSON event per metric publish and stays open until you disconnect.
151
+
152
+ ```bash
153
+ curl -N http://localhost:4000/stream
154
+ ```
155
+
156
+ ```
157
+ data: {"labels":{"building":"hq","floor":"3"},"topic":"hq/3/east/metrics","metric":"temperature","units":"°C","value":22.45,"scenario":"normal","timestamp":1747613001234}
158
+
159
+ data: {"labels":{"building":"hq","floor":"3"},"topic":"hq/3/east/metrics","metric":"humidity","units":"%","value":58.1,"scenario":"normal","timestamp":1747613001235}
160
+ ```
161
+
162
+ Multiple clients can connect simultaneously. The `scenario` field reflects the active scenario at publish time.
163
+
164
+ ### Full snapshot with `/status`
165
+
166
+ ```bash
167
+ curl http://localhost:4000/status
168
+ ```
169
+
170
+ ```json
171
+ {
172
+ "status": "ok",
173
+ "uptime": 142,
174
+ "sources": 1,
175
+ "metrics": 2,
176
+ "scenario": {
177
+ "id": "normal",
178
+ "label": "0 / n — Normal (uses each metric's configured mode)",
179
+ "detail": "Each metric runs its configured mode: sinusoidal, drift, normal, or spike."
180
+ },
181
+ "readings": [
182
+ { "labels": { "building": "hq", "floor": "3" }, "metric": "temperature", "units": "°C", "value": 22.45, "lastPublishedAt": 1747613001234 },
183
+ { "labels": { "building": "hq", "floor": "3" }, "metric": "humidity", "units": "%", "value": 58.1, "lastPublishedAt": 1747613001235 }
184
+ ],
185
+ "effects": {
186
+ "hq/3/east/metrics": { "hvac": false }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ---
192
+
193
+ ## CLI args
194
+
195
+ | Argument | Description |
196
+ |---|---|
197
+ | `--config <path>` | Path to YAML config file |
198
+ | `--log-level <level>` | `silent` \| `error` \| `warn` \| `info` \| `debug` (default: `info`) |
199
+ | `-h`, `--help` | Print help and exit |
200
+
201
+ ```bash
202
+ # Verbose — see every metric publish
203
+ npx mqtt-scenario-sim --config my.yaml --log-level debug
204
+
205
+ # Quiet — errors only
206
+ npx mqtt-scenario-sim --config my.yaml --log-level error
207
+
208
+ # Help
209
+ npx mqtt-scenario-sim --help
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Env vars
215
+
216
+ | Variable | Default | Description |
217
+ |---|---|---|
218
+ | `MQTT_HOST` | `localhost` | MQTT broker host |
219
+ | `MQTT_PORT` | `1883` | MQTT broker port |
220
+ | `PUBLISH_INTERVAL_MS` | `5000` | Default publish interval |
221
+ | `ENCODING` | `json` | `json` or `protobuf` |
222
+ | `PROTO_FILE` | — | Path to `.proto` file |
223
+ | `PROTO_MESSAGE_TYPE` | — | Fully-qualified proto message type |
224
+ | `PROTO_FIELD_MAP` | — | JSON string field map |
225
+ | `CONFIG_PATH` | `examples/minimal.yaml` | Path to YAML config |
226
+ | `PORT` | `4000` | HTTP control plane port |
227
+ | `LOG_LEVEL` | `info` | `silent` \| `error` \| `warn` \| `info` \| `debug` |
228
+
229
+ CLI args take precedence over env vars.
230
+
231
+ ---
232
+
233
+ ## Examples
234
+
235
+ - [`examples/minimal.yaml`](examples/minimal.yaml) — 1 source, 1 metric, JSON
236
+ - [`examples/greenhouse.yaml`](examples/greenhouse.yaml) — multi-source, multi-metric IoT example
237
+ - [`examples/custom-proto.yaml`](examples/custom-proto.yaml) — protobuf with fieldMap
238
+
239
+ ---
240
+
241
+ ## Docker
242
+
243
+ A [`Dockerfile`](Dockerfile) is included. Build and run with your own config:
244
+
245
+ ```bash
246
+ docker build -t mqtt-scenario-sim .
247
+ docker run --rm -e MQTT_HOST=host.docker.internal mqtt-scenario-sim
248
+ ```
249
+
250
+ To use a custom config, mount it at runtime:
251
+
252
+ ```bash
253
+ docker run --rm \
254
+ -v $(pwd)/my-config.yaml:/app/my-config.yaml \
255
+ -e CONFIG_PATH=/app/my-config.yaml \
256
+ -e MQTT_HOST=host.docker.internal \
257
+ mqtt-scenario-sim
258
+ ```
259
+
260
+ ---
261
+
262
+ ## License
263
+
264
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const readline = __importStar(require("readline"));
41
+ const mqtt = __importStar(require("mqtt"));
42
+ const path = __importStar(require("path"));
43
+ const express_1 = __importDefault(require("express"));
44
+ const config_1 = require("./config");
45
+ const encoder_1 = require("./encoder");
46
+ const simulator_1 = require("./simulator");
47
+ const scenarios_1 = require("./scenarios");
48
+ const logger_1 = require("./logger");
49
+ const PORT = parseInt(process.env['PORT'] ?? '4000', 10);
50
+ const logLevelArg = process.argv.find((a) => a.startsWith('--log-level='))?.slice('--log-level='.length)
51
+ ?? (process.argv.indexOf('--log-level') !== -1 ? process.argv[process.argv.indexOf('--log-level') + 1] : undefined)
52
+ ?? process.env['LOG_LEVEL'];
53
+ if (logLevelArg)
54
+ logger_1.logger.setLevel(logLevelArg);
55
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
56
+ console.log(`
57
+ Usage: mqtt-scenario-sim [options]
58
+
59
+ Options:
60
+ --config <path> Path to YAML config file (default: examples/minimal.yaml)
61
+ --log-level <level> Log verbosity: silent | error | warn | info | debug (default: info)
62
+ -h, --help Show this help message
63
+
64
+ Env vars:
65
+ MQTT_HOST MQTT broker host (default: localhost)
66
+ MQTT_PORT MQTT broker port (default: 1883)
67
+ PUBLISH_INTERVAL_MS Publish interval in ms (default: 5000)
68
+ ENCODING json | protobuf (default: json)
69
+ PROTO_FILE Path to .proto file
70
+ PROTO_MESSAGE_TYPE Fully-qualified message type
71
+ PROTO_FIELD_MAP JSON string field map
72
+ CONFIG_PATH Path to YAML config file (default: examples/minimal.yaml)
73
+ PORT HTTP control plane port (default: 4000)
74
+ LOG_LEVEL Log verbosity level (default: info)
75
+
76
+ HTTP API (default port 4000):
77
+ GET /stream SSE stream of live metric publishes
78
+ GET /status Full snapshot: uptime, scenario, readings, effects
79
+ GET /health Status, uptime, source/metric counts, scenario
80
+ GET /scenario Current scenario name + description
81
+ POST /scenario/:id Activate scenario by ID (0–5) or name
82
+ GET /state Last published value per metric
83
+ GET /effects Current effect state per source
84
+
85
+ Scenarios:
86
+ 0 / normal Each metric runs its configured mode
87
+ 1 / out_of_range All metrics 30% above max
88
+ 2 / trending_to_breach Rising toward max
89
+ 3 / stable_healthy Held at midpoint ideal
90
+ 4 / recovery Starts out-of-range, decays to ideal over ~5 min
91
+ 5 / oscillating ±10% swing around ideal
92
+
93
+ Examples:
94
+ mqtt-scenario-sim --config examples/minimal.yaml
95
+ mqtt-scenario-sim --config my.yaml --log-level debug
96
+ `);
97
+ process.exit(0);
98
+ }
99
+ function printMenu() {
100
+ logger_1.logger.info('\n┌─ Scenario control ──────────────────────────────────────────┐');
101
+ for (const label of Object.values(scenarios_1.SCENARIO_LABELS)) {
102
+ logger_1.logger.info(`│ ${label.padEnd(61)}│`);
103
+ }
104
+ logger_1.logger.info('│ │');
105
+ logger_1.logger.info('│ status / ? show active scenario │');
106
+ logger_1.logger.info('│ help re-print this menu │');
107
+ logger_1.logger.info('└─────────────────────────────────────────────────────────────┘\n');
108
+ }
109
+ function resolveScenarioId(input) {
110
+ return scenarios_1.SCENARIO_KEYS[input.trim().toLowerCase()] ?? null;
111
+ }
112
+ async function main() {
113
+ const configArg = process.argv.find((a) => a.startsWith('--config='))?.slice('--config='.length)
114
+ ?? process.argv[process.argv.indexOf('--config') + 1];
115
+ const configPath = configArg
116
+ ? path.resolve(configArg)
117
+ : process.env['CONFIG_PATH'];
118
+ const config = (0, config_1.loadConfig)(configPath);
119
+ const totalMetrics = config.sources.reduce((n, s) => n + s.metrics.length, 0);
120
+ logger_1.logger.info(`[init] ${config.sources.length} source(s) | ${totalMetrics} metric(s) | MQTT: ${config.mqtt.host}:${config.mqtt.port}`);
121
+ const encode = config.encoding.type === 'protobuf'
122
+ ? (0, encoder_1.buildEncoder)(config.encoding)
123
+ : encoder_1.jsonEncoder;
124
+ const brokerUrl = `mqtt://${config.mqtt.host}:${config.mqtt.port}`;
125
+ const client = mqtt.connect(brokerUrl, { reconnectPeriod: 2000 });
126
+ await new Promise((resolve, reject) => {
127
+ client.once('connect', () => resolve());
128
+ client.once('error', (err) => reject(err));
129
+ });
130
+ logger_1.logger.info(`[mqtt] connected to ${brokerUrl}`);
131
+ const sim = (0, simulator_1.startSimulator)(config, client, encode);
132
+ // ── HTTP control plane ────────────────────────────────────────────────────
133
+ const app = (0, express_1.default)();
134
+ const startTime = Date.now();
135
+ app.use(express_1.default.json());
136
+ app.get('/health', (_req, res) => {
137
+ res.json({
138
+ status: 'ok',
139
+ sources: config.sources.length,
140
+ metrics: totalMetrics,
141
+ uptime: Math.floor((Date.now() - startTime) / 1000),
142
+ scenario: sim.getScenario(),
143
+ });
144
+ });
145
+ app.get('/scenario', (_req, res) => {
146
+ const id = sim.getScenario();
147
+ res.json({ scenario: id, label: scenarios_1.SCENARIO_LABELS[id], detail: scenarios_1.SCENARIO_DETAIL[id] });
148
+ });
149
+ app.post('/scenario/:id', (req, res) => {
150
+ const id = resolveScenarioId(req.params['id'] ?? '');
151
+ if (!id) {
152
+ res.status(400).json({
153
+ error: `Unknown scenario "${req.params['id']}". Valid: ${Object.keys(scenarios_1.SCENARIO_KEYS).join(', ')}`,
154
+ });
155
+ return;
156
+ }
157
+ const rawDuration = req.query['durationSeconds'];
158
+ const rawSourceKey = req.query['sourceKey'];
159
+ const durationSeconds = typeof rawDuration === 'string' && rawDuration.length > 0
160
+ ? parseInt(rawDuration, 10)
161
+ : undefined;
162
+ const sourceKey = typeof rawSourceKey === 'string' && rawSourceKey.length > 0
163
+ ? rawSourceKey
164
+ : undefined;
165
+ sim.setScenario(id, sourceKey, durationSeconds && durationSeconds > 0 ? durationSeconds : undefined);
166
+ res.json({
167
+ ok: true,
168
+ scenario: id,
169
+ label: scenarios_1.SCENARIO_LABELS[id],
170
+ ...(sourceKey ? { sourceKey } : { appliedTo: 'all' }),
171
+ ...(durationSeconds && durationSeconds > 0 ? { autoRevertAfterSeconds: durationSeconds } : {}),
172
+ });
173
+ });
174
+ app.get('/state', (_req, res) => {
175
+ res.json(sim.getState());
176
+ });
177
+ app.get('/effects', (_req, res) => {
178
+ res.json(sim.getEffectStates());
179
+ });
180
+ app.get('/stream', (req, res) => {
181
+ res.setHeader('Content-Type', 'text/event-stream');
182
+ res.setHeader('Cache-Control', 'no-cache');
183
+ res.setHeader('Connection', 'keep-alive');
184
+ res.flushHeaders();
185
+ const unsub = sim.onPublish((event) => {
186
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
187
+ });
188
+ req.on('close', unsub);
189
+ });
190
+ app.get('/status', (_req, res) => {
191
+ const scenarioId = sim.getScenario();
192
+ const state = sim.getState();
193
+ res.json({
194
+ status: 'ok',
195
+ uptime: Math.floor((Date.now() - startTime) / 1000),
196
+ sources: config.sources.length,
197
+ metrics: totalMetrics,
198
+ scenario: {
199
+ id: scenarioId,
200
+ label: scenarios_1.SCENARIO_LABELS[scenarioId],
201
+ detail: scenarios_1.SCENARIO_DETAIL[scenarioId],
202
+ },
203
+ readings: state.metrics,
204
+ effects: sim.getEffectStates(),
205
+ });
206
+ });
207
+ const server = app.listen(PORT, () => {
208
+ logger_1.logger.info(`[http] :${PORT}`);
209
+ logger_1.logger.info(` GET /health GET /scenario POST /scenario/:id`);
210
+ logger_1.logger.info(` GET /state GET /effects`);
211
+ });
212
+ server.on('error', (err) => {
213
+ if (err.code === 'EADDRINUSE') {
214
+ logger_1.logger.warn(`[http] port ${PORT} in use — HTTP unavailable, sim still running`);
215
+ }
216
+ else {
217
+ logger_1.logger.error('[http] error:', err);
218
+ }
219
+ });
220
+ // ── Stdin REPL ─────────────────────────────────────────────────────────────
221
+ if (process.stdin.isTTY) {
222
+ printMenu();
223
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'scenario> ' });
224
+ rl.prompt();
225
+ rl.on('line', (line) => {
226
+ const input = line.trim().toLowerCase();
227
+ if (!input) {
228
+ rl.prompt();
229
+ return;
230
+ }
231
+ if (input === 'help') {
232
+ printMenu();
233
+ rl.prompt();
234
+ return;
235
+ }
236
+ if (input === 'status' || input === '?') {
237
+ const id = sim.getScenario();
238
+ logger_1.logger.info(`[scenario] ${id} — ${scenarios_1.SCENARIO_DETAIL[id]}`);
239
+ rl.prompt();
240
+ return;
241
+ }
242
+ const id = resolveScenarioId(input);
243
+ if (id)
244
+ sim.setScenario(id);
245
+ else
246
+ logger_1.logger.info(`Unknown: "${input}". Type help for options.`);
247
+ rl.prompt();
248
+ });
249
+ rl.on('close', () => logger_1.logger.info('[repl] stdin closed'));
250
+ }
251
+ // ── Graceful shutdown ──────────────────────────────────────────────────────
252
+ const shutdown = () => {
253
+ logger_1.logger.info('[shutdown] stopping...');
254
+ sim.stop();
255
+ client.end();
256
+ server.close(() => process.exit(0));
257
+ };
258
+ process.on('SIGTERM', shutdown);
259
+ process.on('SIGINT', shutdown);
260
+ }
261
+ main().catch((err) => {
262
+ logger_1.logger.error('[fatal]', err);
263
+ process.exit(1);
264
+ });
265
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { SensorMode, SensorRange } from './sensors';
2
+ import { EffectConfig } from './effects';
3
+ import { EncodingConfig } from './encoder';
4
+ export interface MetricConfig {
5
+ name: string;
6
+ units: string;
7
+ mode: SensorMode;
8
+ range: SensorRange;
9
+ baseline?: number;
10
+ amplitude?: number;
11
+ periodSeconds?: number;
12
+ driftRate?: number;
13
+ min?: number;
14
+ max?: number;
15
+ intervalMs?: number;
16
+ spikeProbability?: number;
17
+ spikeMagnitude?: number;
18
+ }
19
+ export interface SourceConfig {
20
+ labels: Record<string, string>;
21
+ topic: string;
22
+ metrics: MetricConfig[];
23
+ effects?: EffectConfig[];
24
+ }
25
+ export interface SimulatorConfig {
26
+ mqtt: {
27
+ host: string;
28
+ port: number;
29
+ };
30
+ publishIntervalMs: number;
31
+ encoding: EncodingConfig;
32
+ sources: SourceConfig[];
33
+ }
34
+ declare function resolveTopicTemplate(template: string, labels: Record<string, string>): string;
35
+ export { resolveTopicTemplate };
36
+ export declare function loadConfig(configPath?: string): SimulatorConfig;
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,iBAAS,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEtF;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,eAAe,CAsC/D"}