91 lines
3.1 KiB
JavaScript
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`); }
|
|
}
|