@corvina/device-client
Version:
Corvina NodeJS Device Client
665 lines • 28.8 kB
JavaScript
"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