Files
Digitaltwin/server/ingest/source.js
T

91 lines
3.1 KiB
JavaScript

/**
* 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`); }
}