UNPKG

@corvina/device-client

Version:
665 lines 28.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AlarmSimulator = exports.DataSimulator = exports.BaseSimulator = void 0; const mustache_1 = __importDefault(require("mustache")); const types_1 = require("../common/types"); const ease = require("d3-ease"); const lodash_1 = __importDefault(require("lodash")); const timers_1 = require("timers"); const logger_service_1 = require("./logger.service"); var StepSimulationState; (function (StepSimulationState) { StepSimulationState[StepSimulationState["STABLE"] = 0] = "STABLE"; StepSimulationState[StepSimulationState["TRANSITION"] = 1] = "TRANSITION"; })(StepSimulationState || (StepSimulationState = {})); class BaseSimulator { tag; depsOut; value; lastSentValue; static simulators = new Array(); static simulatorsByTagName; static inited = false; static sorted = false; static intervalID = null; static filterDuplications; static simulationMs; constructor(tag) { this.tag = tag; if (!BaseSimulator.simulatorsByTagName) { BaseSimulator.simulatorsByTagName = new Map(); } BaseSimulator.filterDuplications = !!(() => { try { return JSON.parse(process.env.FILTER_DUPS); } catch (err) { return true; } })(); BaseSimulator.simulationMs = (() => { try { return JSON.parse(process.env.SIMULATION_MS); } catch (err) { return 1000; } })(); if (!BaseSimulator.inited) { BaseSimulator.intervalID = setInterval(() => { if (!BaseSimulator.inited || !BaseSimulator.sorted) { let idx = BaseSimulator.simulatorsByTagName.size; BaseSimulator.simulators = new Array(idx); for (const [k, d] of BaseSimulator.simulatorsByTagName) { d.visited = false; } const visit = (n) => { if (n.visited == true) { return false; } n.visited = true; if (n.depsOut) { for (const [k, v] of n.depsOut) { visit(v); } } idx--; if (idx < 0) { return false; } BaseSimulator.simulators[idx] = n; return true; }; for (const [k, d] of BaseSimulator.simulatorsByTagName) { visit(d); } BaseSimulator.sorted = true; } BaseSimulator.simulators.forEach((value) => { try { value.loop(); } catch (e) { logger_service_1.l.error("Error in simulation:"); logger_service_1.l.error(e); } }); }, BaseSimulator.simulationMs); BaseSimulator.inited = true; } } static $ = (source, tagName) => { const target = BaseSimulator.simulatorsByTagName.get(tagName); if (target == undefined) { logger_service_1.l.error(`Cannot resolve dependency ${tagName}`); return; } if (!target.depsOut) { target.depsOut = new Map(); } if (!target.depsOut.has(source.tag)) { logger_service_1.l.debug(`Tracked dependency from ${source.tag} to ${target.tag}`); target.depsOut.set(source.tag, source); BaseSimulator.sorted = false; } return target.value; }; loop() { } } exports.BaseSimulator = BaseSimulator; class DataSimulator extends BaseSimulator { callback; type; desc; defAmplitude; defPhase; defPeriod; constructor(tag, type, callback, desc) { super(tag); this.type = type; this.desc = desc; DataSimulator.simulatorsByTagName.set(tag, this); if (tag.indexOf(".") >= 0) { const structName = tag.split(".")[0]; let structSimulator = DataSimulator.simulatorsByTagName.get(structName); if (!structSimulator) { structSimulator = new DataSimulator(structName, "struct", callback, desc); } BaseSimulator.$(this, structName); } else { this.callback = callback; } this.defAmplitude = 500 * Math.random(); this.defPhase = Math.random() * 4 * Math.PI; this.defPeriod = Math.random() * 30000; } applyNoise(v, min = -Infinity, max = Infinity) { const props = this.desc; if (props.noise) { const rand = Math.random(); if (typeof v == "boolean") { let noised = Number(v); if (props.noise.type == types_1.NoiseSimulationType.ABSOLUTE) { noised = (~~(noised + rand * props.noise.amplitude)) % 2; } else { noised = (~~(noised + rand * ((noised * props.noise.amplitude) / 100))) % 2; } if (noised < min) { return min; } if (noised > max) { return max; } return !!noised; } let noised = v; if (props.noise.type == types_1.NoiseSimulationType.ABSOLUTE) { noised = noised + (rand - 0.5) * props.noise.amplitude; } else { noised = noised + (rand - 0.5) * ((noised * props.noise.amplitude) / 100); } if (noised < min) { return min; } if (noised > max) { return max; } return noised; } return v; } nullify(v, callback = null) { const props = this.desc; if (props.nullable) { if (!props.nullable.state) { props.nullable.state = { nullifying: false, duration: 0, start: 0, }; } const oldState = props.nullable.state.nullifying; const dice = Math.random(); const now = Date.now(); if (!props.nullable.state.nullifying) { if (dice < props.nullable.probability) { props.nullable.state.nullifying = true; props.nullable.state.start = now; props.nullable.state.duration = BaseSimulator.simulationMs * (props.nullable.dt_min + Math.random() * (props.nullable.dt_max - props.nullable.dt_min)); } } else { if (now > props.nullable.state.start + props.nullable.state.duration) { props.nullable.state.nullifying = false; } } if (callback) { callback(oldState, props.nullable.state.nullifying); } if (props.nullable.state.nullifying == true) { if (typeof v == "number") { return 0; } else if (typeof v == "boolean") { return false; } else { return ""; } } } return v; } async loop() { const ts = Date.now(); this.value = null; if (this.type == "struct") { if (!this.value) { this.value = {}; } Array.from(this.depsOut.values()).forEach((x) => { this.value[x.tag.slice(this.tag.length + 1)] = x.value; }); } else { if (!this.desc) { switch (this.type) { case "integer": this.value = (Math.random() * this.defAmplitude) | 0; break; case "boolean": this.value = Math.random() > 0.5; break; case "double": this.value = this.defAmplitude * Math.sin(this.defPhase + (ts * 2 * Math.PI) / this.defPeriod); break; case "string": this.value = Math.random().toString(); break; case "integerarray": this.value = lodash_1.default.range(0, 10).map((x) => (Math.round(Math.random() * this.defAmplitude))); break; case "doublearray": this.value = lodash_1.default.range(0, 10).map((x) => (Math.random() * this.defAmplitude)); break; case "stringarray": this.value = lodash_1.default.range(0, 10).map((x) => (Number(Math.random() * this.defAmplitude).toFixed())); break; case "booleanarray": this.value = lodash_1.default.range(0, 10).map((x) => Math.random() > 0.5); break; default: throw "Unsupported type " + this.type; } } else { switch (this.desc.type) { case types_1.SimulationType.FUNCTION: { const props = this.desc; try { if (!props._f) { props._f = new Function("$", props.f); } this.value = props._f.call(this, (t) => { return BaseSimulator.$(this, t); }); } catch (e) { logger_service_1.l.error("Error evaluating simulation function:"); logger_service_1.l.error(e); } if (this.value == null || this.value == undefined) { return; } const noised = this.applyNoise(this.value); switch (this.type) { case "integer": this.value = ~~noised; break; case "boolean": this.value = !!noised; break; case "double": this.value = noised; break; case "string": this.value = typeof noised == "string" || noised instanceof String ? noised : JSON.stringify(noised); break; case "integerarray": case "doublearray": this.value = Array.isArray(this.value) ? this.value : [Number(this.value)]; break; case "booleanarray": this.value = Array.isArray(this.value) ? this.value.map(v => v == true) : [this.value == true]; break; default: throw "Unsupported type " + this.type; } this.value = this.nullify(this.value); } break; case types_1.SimulationType.CONST: { const props = this.desc; const noised = this.applyNoise(props.value); switch (this.type) { case "integer": this.value = ~~noised; break; case "boolean": this.value = !!noised; break; case "double": this.value = noised; break; case "string": this.value = typeof noised == "string" || noised instanceof String ? noised : JSON.stringify(noised); break; case "integerarray": case "doublearray": this.value = Array.isArray(this.value) ? this.value : [Number(this.value)]; break; case "booleanarray": this.value = Array.isArray(this.value) ? this.value.map(v => v == true) : [this.value == true]; break; default: throw "Unsupported type " + this.type; } this.value = this.nullify(this.value); } break; case types_1.SimulationType.SINE: { const props = this.desc; const v = this.applyNoise(props.offset + props.amplitude * Math.sin(props.phase + (ts * 2 * Math.PI) / (BaseSimulator.simulationMs * props.period))); switch (this.type) { case "integer": this.value = ~~v; this.value = this.nullify(this.value); break; case "boolean": this.value = !!v; break; case "double": this.value = v; break; case "string": this.value = typeof v == "string" || v instanceof String ? v : JSON.stringify(v); break; default: throw "Unsupported type " + this.type; } this.value = this.nullify(this.value); } break; case types_1.SimulationType.STEP: { const props = this.desc; const f = ease[props.easing]; if (props.easingProps) { Object.assign(f, props.easingProps); } const fun = f; const computeNewTarget = () => { props.state.state = StepSimulationState.TRANSITION; props.state.origin = props.state.current; const rand = Math.random() - 0.5; props.state.target = props.state.origin + rand * props.amplitude; if (props.state.target > props.offset + props.amplitude) { props.state.target = props.state.origin - rand * props.amplitude; } if (props.state.target < props.offset) { props.state.target = props.state.origin - rand * props.amplitude; } const rand2 = Math.random(); props.state.duration = BaseSimulator.simulationMs * (props.dt_min + rand2 * (props.dt_max - props.dt_min)); props.state.start = ts; }; if (!props.state) { props.state = {}; props.state.current = props.offset + props.amplitude / 2; computeNewTarget(); } const jumpRand = Math.random(); if (jumpRand < props.jump_probability) { computeNewTarget(); } let v = props.offset; if (props.state.state == StepSimulationState.TRANSITION) { const dt = ts - props.state.start; if (dt > props.state.duration) { props.state.state = StepSimulationState.STABLE; v = props.state.target; } else { props.state.current = props.state.origin + (props.state.target - props.state.origin) * fun(dt / props.state.duration); v = props.state.current; } } else { v = props.state.current; } const noised = this.applyNoise(v, props.offset, props.offset + props.amplitude); switch (this.type) { case "integer": this.value = ~~noised; break; case "boolean": this.value = !!noised; break; case "double": this.value = noised; break; case "string": this.value = typeof noised == "string" || noised instanceof String ? noised : JSON.stringify(noised); break; default: throw "Unsupported type " + this.type; } this.nullify(this.value, (o, n) => { if (n == true) { this.value = 0; } if (o == true && n == false) { this.value = props.offset; props.state.current = props.offset; computeNewTarget(); } }); } break; } } } if (!BaseSimulator.filterDuplications || JSON.stringify(this.value) != JSON.stringify(this.lastSentValue)) { try { if (this.callback && (await this.callback(this.tag, this.value, ts))) { this.lastSentValue = this.value; } } catch (e) { console.log(e); } } } static clear() { BaseSimulator.sorted = false; BaseSimulator.inited = false; BaseSimulator.simulatorsByTagName && BaseSimulator.simulatorsByTagName.clear(); (0, timers_1.clearInterval)(BaseSimulator.intervalID); } } exports.DataSimulator = DataSimulator; class AlarmSimulator extends BaseSimulator { callback; alarm; alarmData; tagRefs; static alarmSimulatorMapkey(alarmName) { return `Alarm.${alarmName}`; } constructor(alarm, callback) { super(alarm.source); this.alarm = alarm; this.alarm.enabled = true; this.callback = callback; this.alarmData = { sev: this.alarm.severity, tag: this.alarm.source, name: this.alarm.name, state: this.alarm.enabled ? types_1.AlarmState.ALARM_ENABLED : types_1.AlarmState.ALARM_NONE, }; if (this.alarm.desc && this.alarm.desc["en"]) { this.tagRefs = {}; const tokens = mustache_1.default.parse(this.alarm.desc["en"], ["[", "]"]); for (const t of tokens) { if (t[0] == "name") { this.tagRefs[t[1]] = null; } } } BaseSimulator.simulatorsByTagName.set(AlarmSimulator.alarmSimulatorMapkey(alarm.name), this); } acknowledge(evTs, user, comment) { if (evTs != this.alarmData.evTs.valueOf()) { logger_service_1.l.warn(`Trying to reset alarm ${this.alarmData.name}:${evTs} but current active event timestamp is ${this.alarmData.evTs}`); const fakeAck = { name: this.alarmData.name, desc: this.alarmData.desc, ts: new Date(), evTs: new Date(evTs), sev: this.alarmData.sev, tag: this.alarmData.tag, state: types_1.AlarmState.ALARM_ENABLED, }; this.callback(fakeAck); } else { if (this.alarmData.state & types_1.AlarmState.ALARM_REQUIRES_ACK) { this.alarmData.state &= ~types_1.AlarmState.ALARM_REQUIRES_ACK; this.alarmData.state |= types_1.AlarmState.ALARM_ACKED; if (this.alarm.reset_required) { this.alarmData.state |= types_1.AlarmState.ALARM_REQUIRES_RESET; } logger_service_1.l.info(`Alarm ${this.alarmData.name} acknowledged by ${user} : ${comment}`); this.alarmData.ts = new Date(); this.propagate(); } else { logger_service_1.l.warn(`Alarm ${this.alarmData.name} does not require ack`); } } } reset(evTs, user, comment) { if (evTs != this.alarmData.evTs.valueOf()) { logger_service_1.l.warn(`Trying to reset alarm ${this.alarmData.name}:${evTs} but current active event timestamp is ${this.alarmData.evTs}`); const fakeReset = { name: this.alarmData.name, desc: this.alarmData.desc, ts: new Date(), evTs: new Date(evTs), sev: this.alarmData.sev, tag: this.alarmData.tag, state: types_1.AlarmState.ALARM_ENABLED, }; this.callback(fakeReset); } else { if (this.alarmData.state & types_1.AlarmState.ALARM_REQUIRES_RESET) { if (!(this.alarmData.state & types_1.AlarmState.ALARM_ACTIVE)) { this.alarmData.state &= ~(types_1.AlarmState.ALARM_ACKED | types_1.AlarmState.ALARM_REQUIRES_RESET); logger_service_1.l.info(`Alarm ${this.alarmData.name} reset by ${user} : ${comment}`); this.alarmData.ts = new Date(); this.propagate(); } else { logger_service_1.l.warn(`Cannot reset active alarm ${this.alarmData.name}`); } } else { logger_service_1.l.warn(`Alarm ${this.alarmData.name} does not require reset`); } } } disable() { this.alarmData.state &= ~(types_1.AlarmState.ALARM_ENABLED | types_1.AlarmState.ALARM_ACKED | types_1.AlarmState.ALARM_REQUIRES_ACK | types_1.AlarmState.ALARM_REQUIRES_RESET); } enable() { this.alarmData.state |= types_1.AlarmState.ALARM_ENABLED; this.loop(); } async propagate() { try { const tagValue = BaseSimulator.$(this, this.alarm.source); switch (typeof tagValue) { case "number": this.alarmData.v_d = tagValue; break; case "string": this.alarmData.v_s = tagValue; break; case "boolean": this.alarmData.v_b = tagValue; break; default: throw "Unsupported type " + typeof tagValue; } if (this.tagRefs) { for (const r in this.tagRefs) { this.tagRefs[r] = BaseSimulator.$(this, r); } this.alarmData.desc = mustache_1.default.render(this.alarm.desc["en"], this.tagRefs, {}, ["[", "]"]); } if (await this.callback(this.alarmData)) { logger_service_1.l.debug("Updated alarm value %j %j %j", this.lastSentValue, this.value, this.alarmData); } } catch (e) { logger_service_1.l.error("Error propagating data"); logger_service_1.l.error(e); } } async loop() { if (!this.alarm.simulation) { return; } const ts = Date.now(); this.value = null; { const props = this.alarm.simulation; try { if (!props._f) { props._f = new Function("$", "$src", props.f); } this.value = props._f.call(this, (t) => { return BaseSimulator.$(this, t), BaseSimulator.$(this, this.alarm.source); }); } catch (e) { logger_service_1.l.error("Error evaluating alarm function"); logger_service_1.l.error(e); } if (this.value == null || this.value == undefined) { return; } } const changedValue = JSON.stringify(this.value) != JSON.stringify(this.lastSentValue); if (changedValue) { if (this.value) { this.alarmData.state |= types_1.AlarmState.ALARM_ACTIVE; } else { this.alarmData.state &= ~types_1.AlarmState.ALARM_ACTIVE; } if (this.alarmData.state & types_1.AlarmState.ALARM_ACTIVE && !(this.alarmData.state & (types_1.AlarmState.ALARM_REQUIRES_ACK | types_1.AlarmState.ALARM_REQUIRES_RESET))) { this.alarmData.evTs = new Date(); this.alarmData.ts = this.alarmData.evTs; } else { this.alarmData.ts = new Date(); } if (this.alarm.ack_required && this.value && !(this.alarmData.state & types_1.AlarmState.ALARM_ACKED)) { this.alarmData.state |= types_1.AlarmState.ALARM_REQUIRES_ACK; } if (!this.alarm.enabled && !this.value) { this.alarmData.state &= ~types_1.AlarmState.ALARM_REQUIRES_ACK; } if (this.alarm.enabled && this.alarmData.evTs) { await this.propagate(); } this.lastSentValue = this.value; } } } exports.AlarmSimulator = AlarmSimulator; //# sourceMappingURL=simulation.js.map