/** * The telemetry ingest seam. * * Everything downstream of this interface - analytics, alarms, the API, the UI, * the copilot - only ever sees frames. It has no idea whether those frames came * from a simulator, an MQTT broker, or an OPC-UA server. * * That is the whole point of putting a seam here rather than running the * simulator in the browser: "can it take our data?" is answered by implementing * one class, not by rewriting the application. * * A frame is the ProductionLine snapshot plus the analytics block: * { t, wallT, lineId, stations[], buffers[], bufferCapacity, kpi, faults[], * controls, totals, analytics: { alarms[], predictions[], trends[] }, * sim: { speed, paused, tickMs, source } } */ /** Frames of history retained for replay to newly connected clients. */ export const REPLAY_FRAMES = 240; export class TelemetrySource { constructor(name) { this.name = name; this.listeners = new Set(); this.latest = null; /** * A short ring of recent frames, trimmed to what the charts need. * * Without this, opening the dashboard gives you empty trend charts that take * minutes of wall-clock to fill - so the first thing a customer sees is a * dashboard with no history on it. Replaying this on connect means the charts * are populated the instant the page loads. */ this.replay = []; } /** * What this source supports. The UI hides controls a source cannot honour, so * a read-only historian replay does not show fault-injection buttons that * would silently do nothing. */ get capabilities() { return { timeControl: false, faultInjection: false, setpointControl: false }; } onFrame(cb) { this.listeners.add(cb); return () => this.listeners.delete(cb); } emit(frame) { this.latest = frame; this.replay.push({ t: frame.t, kpi: { oee: frame.kpi.oee, availability: frame.kpi.availability, performance: frame.kpi.performance, quality: frame.kpi.quality, }, stations: frame.stations.map((s) => ({ id: s.id, online: s.online, signals: s.signals })), }); if (this.replay.length > REPLAY_FRAMES) this.replay.shift(); for (const cb of this.listeners) { try { cb(frame); } catch (err) { console.error(`[${this.name}] frame listener failed:`, err); } } } async start() { throw new Error(`${this.name}: start() not implemented`); } async stop() {} // --- optional control surface; sources that cannot do these should throw --- setSpeed() { throw new Error(`${this.name} does not support time control`); } setSetpoint() { throw new Error(`${this.name} does not support setpoint control`); } setLineSpeed() { throw new Error(`${this.name} does not support line speed control`); } injectFault() { throw new Error(`${this.name} does not support fault injection`); } clearFault() { throw new Error(`${this.name} does not support fault injection`); } toolChange() { throw new Error(`${this.name} does not support maintenance actions`); } reset() { throw new Error(`${this.name} does not support reset`); } }