Initial commit with Dockerfile and demo code
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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`); }
|
||||
}
|
||||
Reference in New Issue
Block a user