Initial commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* The simulated telemetry source: owns the model, the clock, and the analytics.
|
||||
*
|
||||
* Real time advances at TICK_MS. Simulated time advances at TICK_MS * speed, so
|
||||
* the operator can run the plant at 60x and watch a twenty-minute degradation
|
||||
* play out in twenty seconds. Nothing downstream knows or cares.
|
||||
*/
|
||||
|
||||
import { ProductionLine } from '../sim/line.js';
|
||||
import { AnalyticsEngine } from '../analytics/alarms.js';
|
||||
import { TelemetrySource } from './source.js';
|
||||
|
||||
/** Real milliseconds between broadcast frames. */
|
||||
export const TICK_MS = 500;
|
||||
|
||||
/** Selectable clock multipliers. 0 is paused. */
|
||||
export const SPEEDS = [0, 1, 5, 20, 60];
|
||||
|
||||
/**
|
||||
* Simulated seconds to run before serving the first frame.
|
||||
*
|
||||
* The demo must open on a plant that has been running, not one that just booted.
|
||||
* Without this the rolling OEE window contains a few seconds of loss-free data and
|
||||
* reads a perfect 100%, which is precisely the "obviously fabricated" impression
|
||||
* the model works hard to avoid; the learned anomaly baselines are also not ready,
|
||||
* so nothing can be detected for the first several minutes.
|
||||
*/
|
||||
export const PREWARM_SEC = 1800;
|
||||
|
||||
export class SimulatedSource extends TelemetrySource {
|
||||
constructor({ seed, prewarmSec } = {}) {
|
||||
super('simulated');
|
||||
this.line = new ProductionLine(seed);
|
||||
this.analytics = new AnalyticsEngine();
|
||||
this.speed = 1;
|
||||
this.timer = null;
|
||||
this.prewarmSec = Number.isFinite(prewarmSec) ? prewarmSec : PREWARM_SEC;
|
||||
}
|
||||
|
||||
get capabilities() {
|
||||
return { timeControl: true, faultInjection: true, setpointControl: true };
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.timer) return;
|
||||
|
||||
if (this.prewarmSec > 0) {
|
||||
const t0 = Date.now();
|
||||
const step = (TICK_MS / 1000);
|
||||
const iterations = Math.floor(this.prewarmSec / step);
|
||||
// Run through the normal tick path so the replay ring and the analytics
|
||||
// baselines end up in exactly the state they would reach organically.
|
||||
for (let i = 0; i < iterations; i++) this.tick(step);
|
||||
console.log(
|
||||
`[source] pre-warmed ${(this.prewarmSec / 60).toFixed(0)} simulated minutes in ${Date.now() - t0} ms`,
|
||||
);
|
||||
} else {
|
||||
// At minimum emit one frame so a connecting client has something to render.
|
||||
this.tick(0);
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => this.tick((TICK_MS / 1000) * this.speed), TICK_MS);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
tick(simSeconds) {
|
||||
if (simSeconds > 0) this.line.step(simSeconds);
|
||||
const snap = this.line.snapshot();
|
||||
const analytics = this.analytics.update(snap);
|
||||
this.emit({
|
||||
...snap,
|
||||
analytics,
|
||||
events: this.line.events.slice(-40).reverse(),
|
||||
sim: {
|
||||
speed: this.speed,
|
||||
paused: this.speed === 0,
|
||||
speeds: SPEEDS,
|
||||
tickMs: TICK_MS,
|
||||
source: this.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setSpeed(speed) {
|
||||
const s = Number(speed);
|
||||
if (!SPEEDS.includes(s)) throw new Error(`Unsupported speed ${speed}. Allowed: ${SPEEDS.join(', ')}`);
|
||||
this.speed = s;
|
||||
this.line.logEvent('action', 'LINE-1', s === 0 ? 'Simulation paused.' : `Simulation speed set to ${s}x.`);
|
||||
return s;
|
||||
}
|
||||
|
||||
setSetpoint(v) { return this.line.setSetpoint(v); }
|
||||
setLineSpeed(v) { return this.line.setLineSpeed(v); }
|
||||
injectFault(id) { return this.line.injectFault(id); }
|
||||
clearFault(id) { return this.line.clearFault(id); }
|
||||
toolChange() { return this.line.toolChange(); }
|
||||
|
||||
reset() {
|
||||
this.line.reset();
|
||||
// The learned baselines belong to the old run; keeping them would flag the
|
||||
// fresh line as anomalous.
|
||||
this.analytics.reset();
|
||||
this.replay = [];
|
||||
|
||||
// Re-warm, or Reset would leave the dashboard showing a perfect 100% OEE and
|
||||
// an anomaly detector with nothing learned - worse than before the reset.
|
||||
if (this.prewarmSec > 0) {
|
||||
const step = TICK_MS / 1000;
|
||||
const iterations = Math.floor(this.prewarmSec / step);
|
||||
for (let i = 0; i < iterations; i++) this.tick(step);
|
||||
} else {
|
||||
this.tick(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user