/** * MQTT telemetry source - DOCUMENTED STUB. * * This is not implemented, and it says so honestly rather than pretending. What * it does provide is the exact shape of the work: the tag map, the frame * assembly, and where analytics plugs in. Wiring this to a real broker is a * day's work, not a rewrite, because everything downstream consumes frames. * * To implement: * 1. npm i mqtt * 2. Fill TAG_MAP with the customer's actual topic names. * 3. Implement start() as marked below. * 4. Set TELEMETRY_SOURCE=mqtt and MQTT_URL in .env * * Sparkplug B note: most industrial brokers publish Sparkplug B protobuf rather * than plain JSON on flat topics. If so, add `sparkplug-payload` to decode * NBIRTH/NDATA messages and map metric aliases instead of topic strings. */ import { TelemetrySource } from './source.js'; import { AnalyticsEngine } from '../analytics/alarms.js'; /** * Maps a broker topic to a station signal. * * The demo model expects the signals declared in server/sim/stations.js. Any * topic not mapped here is ignored; any signal not supplied by the broker simply * has no data, and the UI shows it as such rather than inventing a value. */ export const TAG_MAP = { // 'plant/line1/conveyor01/belt_speed': { station: 'CONV-01', signal: 'beltSpeed' }, // 'plant/line1/conveyor01/motor_current': { station: 'CONV-01', signal: 'motorAmps' }, // 'plant/line1/cnc02/vibration_rms': { station: 'CNC-02', signal: 'vibration' }, // 'plant/line1/cnc02/spindle_load': { station: 'CNC-02', signal: 'spindleLoad' }, // 'plant/line1/oven03/zone2_pv': { station: 'OVN-03', signal: 'zone2Temp' }, // 'plant/line1/oven03/zone2_sp': { station: 'OVN-03', signal: 'setpoint' }, // 'plant/line1/ins04/reject_rate': { station: 'INS-04', signal: 'rejectRate' }, // 'plant/line1/pkg05/units_per_min': { station: 'PKG-05', signal: 'unitsPerMin' }, }; export class MqttSource extends TelemetrySource { constructor({ url, username, password, topicPrefix } = {}) { super('mqtt'); this.url = url; this.username = username; this.password = password; this.topicPrefix = topicPrefix; this.analytics = new AnalyticsEngine(); } /** A real broker feed is read-only: you observe the plant, you do not drive it. */ get capabilities() { return { timeControl: false, faultInjection: false, setpointControl: false }; } async start() { throw new Error( 'MQTT source is not configured. This is a documented stub.\n' + 'To enable it: npm i mqtt, populate TAG_MAP in server/ingest/mqttSource.js ' + 'with your topic names, implement start(), then set TELEMETRY_SOURCE=mqtt ' + 'and MQTT_URL in .env.\n' + 'Run with TELEMETRY_SOURCE=simulated for the demo.', ); /* Implementation outline: * * const mqtt = await import('mqtt'); * this.client = mqtt.connect(this.url, { username: this.username, password: this.password }); * this.client.on('connect', () => this.client.subscribe(Object.keys(TAG_MAP))); * * // Accumulate the latest value per tag. Industrial tags publish on change, * // at wildly different rates, so you assemble a frame on a timer rather * // than trying to emit one per message. * this.client.on('message', (topic, payload) => { * const tag = TAG_MAP[topic]; * if (!tag) return; * this.values[`${tag.station}.${tag.signal}`] = Number(payload.toString()); * }); * * this.timer = setInterval(() => this.assembleAndEmit(), 500); * * assembleAndEmit() builds the same frame shape SimulatedSource emits: * stations with their signals, KPI rollups (see server/sim/kpi.js - the * KpiTracker works on any counter source, not just the simulator), then * this.analytics.update(snapshot) and this.emit(frame). * * Two things that bite in the real world: * - Staleness. Track a per-tag last-seen timestamp and mark a station * offline when its tags go quiet, exactly as the F4 dropout fault does. * Never let a stale value render as if it were live. * - Units. Vibration in in/s, temperature in F, and pressure in psi are all * common. Convert at the boundary here, not downstream. */ } async stop() { if (this.timer) clearInterval(this.timer); if (this.client) this.client.end(); } }