@machinemetrics/io-adapter-lib 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.circleci/config.yml +141 -0
  2. package/.eslintrc.json +36 -0
  3. package/.gitattributes +12 -0
  4. package/CHANGELOG.md +544 -0
  5. package/README.md +2 -0
  6. package/index.js +17 -0
  7. package/lib/config/adapterConfig.js +535 -0
  8. package/lib/config/common/allowDenyList.js +58 -0
  9. package/lib/config/common/jsonPath.js +44 -0
  10. package/lib/config/common/labjackU3T4Common.js +227 -0
  11. package/lib/config/common/validations.js +142 -0
  12. package/lib/config/configError.js +32 -0
  13. package/lib/config/device/adam-6052Config.js +103 -0
  14. package/lib/config/device/brotherHTTPConfig.js +215 -0
  15. package/lib/config/device/ethernetIPConfig.js +191 -0
  16. package/lib/config/device/ifmIotConfig.js +245 -0
  17. package/lib/config/device/jsonHttpConfig.js +76 -0
  18. package/lib/config/device/labjackT4Config.js +39 -0
  19. package/lib/config/device/labjackT7Config.js +192 -0
  20. package/lib/config/device/labjackU3Config.js +32 -0
  21. package/lib/config/device/modbusTcpConfig.js +336 -0
  22. package/lib/config/device/mqttBaseConfig.js +70 -0
  23. package/lib/config/device/mqttConfig.js +113 -0
  24. package/lib/config/device/mqttLincolnConfig.js +62 -0
  25. package/lib/config/device/mtconnectAdapterConfig.js +42 -0
  26. package/lib/config/device/mtconnectBaseConfig.js +136 -0
  27. package/lib/config/device/mtconnectConfig.js +52 -0
  28. package/lib/config/device/mtconnectHaasConfig.js +15 -0
  29. package/lib/config/device/nullConfig.js +81 -0
  30. package/lib/config/device/opcuaConfig.js +205 -0
  31. package/lib/config/device/pcccConfig.js +88 -0
  32. package/lib/config/engineConfigV1.js +382 -0
  33. package/lib/config/engineConfigV2.js +106 -0
  34. package/lib/config/generator/counterGenConfig.js +15 -0
  35. package/lib/config/generator/cronGenConfig.js +33 -0
  36. package/lib/config/generator/dateTimeGenConfig.js +33 -0
  37. package/lib/config/index.js +339 -0
  38. package/lib/config/transformConfigUtil.js +296 -0
  39. package/lib/engine/dataOutput.js +357 -0
  40. package/lib/engine/deviceOutput.js +186 -0
  41. package/lib/engine/engineV1.js +480 -0
  42. package/lib/engine/engineV2.js +719 -0
  43. package/lib/engine/index.js +34 -0
  44. package/lib/engine/transformBuilderV1.js +111 -0
  45. package/lib/engine/transformBuilderV2.js +74 -0
  46. package/lib/expressionService.js +330 -0
  47. package/lib/math.js +142 -0
  48. package/lib/transform/accumulate.js +98 -0
  49. package/lib/transform/average.js +56 -0
  50. package/lib/transform/debounce.js +152 -0
  51. package/lib/transform/downsample.js +69 -0
  52. package/lib/transform/edge.js +34 -0
  53. package/lib/transform/expression.js +91 -0
  54. package/lib/transform/fallingEdge.js +70 -0
  55. package/lib/transform/fromBuffer.js +89 -0
  56. package/lib/transform/hash.js +41 -0
  57. package/lib/transform/ignoreValue.js +118 -0
  58. package/lib/transform/index.js +93 -0
  59. package/lib/transform/invert.js +25 -0
  60. package/lib/transform/latch.js +99 -0
  61. package/lib/transform/latchValue.js +115 -0
  62. package/lib/transform/logicAnd.js +67 -0
  63. package/lib/transform/logicOr.js +67 -0
  64. package/lib/transform/map.js +115 -0
  65. package/lib/transform/max.js +57 -0
  66. package/lib/transform/maxLength.js +39 -0
  67. package/lib/transform/min.js +57 -0
  68. package/lib/transform/minDelta.js +40 -0
  69. package/lib/transform/offDelay.js +89 -0
  70. package/lib/transform/onDelay.js +89 -0
  71. package/lib/transform/patternEscape.js +24 -0
  72. package/lib/transform/patternMatch.js +194 -0
  73. package/lib/transform/patternReplace.js +170 -0
  74. package/lib/transform/patternTest.js +85 -0
  75. package/lib/transform/rateOfChange.js +56 -0
  76. package/lib/transform/reject.js +115 -0
  77. package/lib/transform/resample.js +96 -0
  78. package/lib/transform/risingEdge.js +90 -0
  79. package/lib/transform/risingEdgeCounter.js +179 -0
  80. package/lib/transform/sampleInterval.js +12 -0
  81. package/lib/transform/source.js +116 -0
  82. package/lib/transform/state.js +201 -0
  83. package/lib/transform/threshold.js +52 -0
  84. package/lib/transform/toBuffer.js +159 -0
  85. package/lib/transform/toggle.js +118 -0
  86. package/lib/transform/transformState.js +193 -0
  87. package/lib/transform/trim.js +27 -0
  88. package/lib/transform/util/chainSource.js +96 -0
  89. package/lib/transform/util/ringBuffer.js +34 -0
  90. package/lib/transform/valueChange.js +34 -0
  91. package/lib/transform/valueDecrease.js +38 -0
  92. package/lib/transform/valueIncrease.js +38 -0
  93. package/lib/transform/valueIncreaseDiff.js +66 -0
  94. package/lib/transform/whenUnavailable.js +73 -0
  95. package/lib/transform/windowCount.js +86 -0
  96. package/lib/util/fileUtil.js +44 -0
  97. package/package.json +38 -0
  98. package/test/.eslintrc.json +15 -0
  99. package/test/chainedTransform.test.js +88 -0
  100. package/test/conditions.test.js +118 -0
  101. package/test/config/ab-pccc.test.js +41 -0
  102. package/test/config/adam-6052.test.js +16 -0
  103. package/test/config/adapter.test.js +109 -0
  104. package/test/config/brother-http.test.js +19 -0
  105. package/test/config/ethernet-ip.test.js +19 -0
  106. package/test/config/ifm-iot.test.js +20 -0
  107. package/test/config/json-http.test.js +47 -0
  108. package/test/config/labjack-t4.test.js +78 -0
  109. package/test/config/labjack-t7.test.js +18 -0
  110. package/test/config/labjack-u3.test.js +17 -0
  111. package/test/config/modbusTcp.test.js +87 -0
  112. package/test/config/mqtt.test.js +19 -0
  113. package/test/config/mqttLincoln.test.js +29 -0
  114. package/test/config/mtconnect.test.js +63 -0
  115. package/test/config/mtconnectAdapter.test.js +124 -0
  116. package/test/config/mtconnectHaas.test.js +15 -0
  117. package/test/config/null.test.js +16 -0
  118. package/test/config/opcua.test.js +97 -0
  119. package/test/config-tests.js +102 -0
  120. package/test/configFiles/conditions.yml +37 -0
  121. package/test/configFiles/data-items-legacy.yml +24 -0
  122. package/test/configFiles/data-items-shorthand.yml +14 -0
  123. package/test/configFiles/data-items.yml +12 -0
  124. package/test/configFiles/device/ab-pccc-default.yml +3 -0
  125. package/test/configFiles/device/ab-pccc-full.yml +13 -0
  126. package/test/configFiles/device/adam-6052-default.yml +2 -0
  127. package/test/configFiles/device/brother-http-default.yml +3 -0
  128. package/test/configFiles/device/ethernet-ip-default.yml +2 -0
  129. package/test/configFiles/device/ifm-iot-default.yml +2 -0
  130. package/test/configFiles/device/json-http-bad-prop.yml +13 -0
  131. package/test/configFiles/device/json-http-bad-prop2.yml +13 -0
  132. package/test/configFiles/device/json-http-default.yml +3 -0
  133. package/test/configFiles/device/json-http-std.yml +13 -0
  134. package/test/configFiles/device/labjack-t4-condition-exprs.yaml +46 -0
  135. package/test/configFiles/device/labjack-t4-default.yml +3 -0
  136. package/test/configFiles/device/labjack-t4-pins-alt.yaml +41 -0
  137. package/test/configFiles/device/labjack-t4-pins.yaml +40 -0
  138. package/test/configFiles/device/labjack-t4-v1.yaml +29 -0
  139. package/test/configFiles/device/labjack-t7-default.yml +2 -0
  140. package/test/configFiles/device/labjack-u3-default.yml +2 -0
  141. package/test/configFiles/device/modbus-partial.yml +4 -0
  142. package/test/configFiles/device/modbus-std-extended.yaml +23 -0
  143. package/test/configFiles/device/modbus-std.yaml +34 -0
  144. package/test/configFiles/device/modbus-tcp-default.yml +3 -0
  145. package/test/configFiles/device/mqtt-default.yml +2 -0
  146. package/test/configFiles/device/mqtt-lincoln-default.yml +3 -0
  147. package/test/configFiles/device/mqtt-lincoln-full.yml +7 -0
  148. package/test/configFiles/device/mtconnect-adapter-default.yml +3 -0
  149. package/test/configFiles/device/mtconnect-adapter-keys.yaml +18 -0
  150. package/test/configFiles/device/mtconnect-complex-keys.yaml +17 -0
  151. package/test/configFiles/device/mtconnect-default.yml +3 -0
  152. package/test/configFiles/device/mtconnect-duplicate-allow-keys.yaml +10 -0
  153. package/test/configFiles/device/mtconnect-duplicate-declare-keys.yaml +8 -0
  154. package/test/configFiles/device/mtconnect-duplicate-deny-keys.yaml +10 -0
  155. package/test/configFiles/device/mtconnect-haas-default.yml +3 -0
  156. package/test/configFiles/device/mtconnect-std.yaml +18 -0
  157. package/test/configFiles/device/null-default.yml +1 -0
  158. package/test/configFiles/device/opcua-bad-tag.yml +18 -0
  159. package/test/configFiles/device/opcua-bad-tag2.yml +18 -0
  160. package/test/configFiles/device/opcua-default.yml +3 -0
  161. package/test/configFiles/device/opcua-std.yml +18 -0
  162. package/test/configFiles/dump-test.yml +11 -0
  163. package/test/configFiles/expressionCond.yml +46 -0
  164. package/test/configFiles/min-config-t4.yaml +4 -0
  165. package/test/configFiles/min-config-u3.yaml +3 -0
  166. package/test/configFiles/missing-device.yaml +2 -0
  167. package/test/configFiles/parse-error1.yml +9 -0
  168. package/test/configFiles/parse-error2.yml +9 -0
  169. package/test/configFiles/repro/buffer-convert-repro.yml +15 -0
  170. package/test/configFiles/repro/chained-delay-timing-repro.yml +13 -0
  171. package/test/configFiles/repro/count-init-repro.yml +45 -0
  172. package/test/configFiles/repro/cycle-break-repro.yml +44 -0
  173. package/test/configFiles/repro/debounce-repro.yml +46 -0
  174. package/test/configFiles/repro/diff-count-repro.yml +34 -0
  175. package/test/configFiles/repro/engine-hang-repro.yml +9 -0
  176. package/test/configFiles/repro/latch-apm-repro.yml +26 -0
  177. package/test/configFiles/repro/lockout-count-repro.yml +33 -0
  178. package/test/configFiles/repro/program-extract-repro.yml +38 -0
  179. package/test/configFiles/repro/state-latch-repro.yml +47 -0
  180. package/test/configFiles/repro/ternary-repro.yml +26 -0
  181. package/test/configFiles/transform/debounce.yml +12 -0
  182. package/test/configFiles/transform/expression.yml +34 -0
  183. package/test/configFiles/transform/ignoreValue.yml +31 -0
  184. package/test/configFiles/transform/latch.yml +11 -0
  185. package/test/configFiles/transform/latchValue.yml +31 -0
  186. package/test/configFiles/transform/logicAnd.yml +14 -0
  187. package/test/configFiles/transform/logicOr.yml +14 -0
  188. package/test/configFiles/transform/map.yml +19 -0
  189. package/test/configFiles/transform/maxLength.yml +13 -0
  190. package/test/configFiles/transform/offDelay.yml +12 -0
  191. package/test/configFiles/transform/pattern-escape.yml +10 -0
  192. package/test/configFiles/transform/pattern-match.yml +57 -0
  193. package/test/configFiles/transform/pattern-replace.yml +34 -0
  194. package/test/configFiles/transform/pattern-test.yml +25 -0
  195. package/test/configFiles/transform/reject.yml +24 -0
  196. package/test/configFiles/transform/risingEdgeCounter.yml +36 -0
  197. package/test/configFiles/transform/source.yml +20 -0
  198. package/test/configFiles/transform/state.yml +56 -0
  199. package/test/configFiles/transform/toggle.yml +19 -0
  200. package/test/configFiles/transform/whenUnavailable.yml +19 -0
  201. package/test/dataFiles/noisy-pulse.txt +11330 -0
  202. package/test/dataItems.test.js +140 -0
  203. package/test/engine-v1-tests.js +418 -0
  204. package/test/engine-v2-tests.js +284 -0
  205. package/test/expression-tests.js +171 -0
  206. package/test/expressionService.test.js +154 -0
  207. package/test/expressionServiceCondition.test.js +130 -0
  208. package/test/repro/buffer-convert-repro.test.js +38 -0
  209. package/test/repro/chained-delay-timing-repro.test.js +34 -0
  210. package/test/repro/count-init-repro.test.js +46 -0
  211. package/test/repro/cylce-break-repro.test.js +57 -0
  212. package/test/repro/debounce-repro.test.js +65 -0
  213. package/test/repro/diff-count-repro.test.js +79 -0
  214. package/test/repro/engine-hang-repro.test.js +38 -0
  215. package/test/repro/latch-apm-repro.test.js +119 -0
  216. package/test/repro/lockout-count-repro.test.js +84 -0
  217. package/test/repro/program-extract-repro.test.js +40 -0
  218. package/test/repro/state-latch-repro.test.js +63 -0
  219. package/test/repro/ternary-repro.test.js +43 -0
  220. package/test/transform/accumulte.test.js +18 -0
  221. package/test/transform/average.test.js +22 -0
  222. package/test/transform/debounce.test.js +70 -0
  223. package/test/transform/downsample.test.js +30 -0
  224. package/test/transform/edge.test.js +27 -0
  225. package/test/transform/expression.test.js +189 -0
  226. package/test/transform/fallingEdge.test.js +59 -0
  227. package/test/transform/fromBuffer.test.js +60 -0
  228. package/test/transform/hash.test.js +34 -0
  229. package/test/transform/ignoreValue.test.js +123 -0
  230. package/test/transform/invert.test.js +26 -0
  231. package/test/transform/latch.test.js +33 -0
  232. package/test/transform/latchValue.test.js +126 -0
  233. package/test/transform/logicAnd.test.js +80 -0
  234. package/test/transform/logicOr.test.js +80 -0
  235. package/test/transform/map.test.js +42 -0
  236. package/test/transform/max.test.js +30 -0
  237. package/test/transform/maxLength.test.js +32 -0
  238. package/test/transform/min.test.js +30 -0
  239. package/test/transform/minDelta.test.js +14 -0
  240. package/test/transform/offDelay.test.js +123 -0
  241. package/test/transform/onDelay.test.js +105 -0
  242. package/test/transform/patternEscape.test.js +18 -0
  243. package/test/transform/patternMatch.test.js +177 -0
  244. package/test/transform/patternReplace.test.js +95 -0
  245. package/test/transform/patternTest.test.js +105 -0
  246. package/test/transform/rateOfChange.test.js +34 -0
  247. package/test/transform/reject.test.js +56 -0
  248. package/test/transform/resample.test.js +193 -0
  249. package/test/transform/risingEdge.test.js +60 -0
  250. package/test/transform/risingEdgeCounter.test.js +227 -0
  251. package/test/transform/sampleInterval.test.js +22 -0
  252. package/test/transform/source.test.js +137 -0
  253. package/test/transform/state.test.js +248 -0
  254. package/test/transform/threshold.test.js +78 -0
  255. package/test/transform/toBuffer.test.js +60 -0
  256. package/test/transform/toggle.test.js +92 -0
  257. package/test/transform/trim.test.js +30 -0
  258. package/test/transform/valueChange.test.js +14 -0
  259. package/test/transform/valueDecrease.test.js +32 -0
  260. package/test/transform/valueIncrease.test.js +32 -0
  261. package/test/transform/valueIncreaseDiff.test.js +32 -0
  262. package/test/transform/whenUnavailable.test.js +93 -0
  263. package/test/transform/windowCount.test.js +26 -0
  264. package/test/util/testUtils.js +405 -0
@@ -0,0 +1,719 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const EventEmitter = require('eventemitter3');
5
+
6
+ class EngineV2 extends EventEmitter {
7
+ constructor(config = {}, options = {}) {
8
+ super();
9
+ this.pool = {};
10
+ this.inputSource = {};
11
+ this.pendingAttach = [];
12
+ this.updateFuncs = {};
13
+ this.sources = [];
14
+
15
+ this.expressionService = config.expressionService;
16
+
17
+ // References variable chains directly
18
+ this.variablePool = {};
19
+ this.priorityPool = {};
20
+
21
+ this.nextVariableIndex = 0;
22
+ this.lastUpdateTime = 0;
23
+
24
+ this.logDir = options.logDir || '.';
25
+ this.logger = options.logger || {
26
+ error: () => {},
27
+ warn: () => {},
28
+ info: () => {},
29
+ };
30
+
31
+ this.deviceOutput = options.deviceOutput;
32
+
33
+ this.initSystem();
34
+ this.resetStatistics();
35
+ }
36
+
37
+ resetStatistics() {
38
+ this.stats = {
39
+ currentStateCalls: 0,
40
+ updateCyclesDetected: 0,
41
+ unavailCyclesDetected: 0,
42
+ };
43
+ }
44
+
45
+ initSystem() {
46
+ _.each(this.expressionService.definedNames('system'), (name) => {
47
+ if (!this.pool[name]) {
48
+ const state = this.createStandardState(name);
49
+ this.pool[name] = state;
50
+ this.emit('add-item', state);
51
+ }
52
+ });
53
+ }
54
+
55
+ addVariable(name, chain) {
56
+ const state = this.createStandardState(name);
57
+ state.isVariable = true;
58
+
59
+ state.variableOrder = this.nextVariableIndex;
60
+ this.nextVariableIndex += 1;
61
+
62
+ chain.chainEnd().on('update', (value, time, context) => {
63
+ state.updateFromChain(value, time, { context });
64
+ });
65
+ chain.chainEnd().on('unavailable', (time, context) => {
66
+ state.setUnavailable(time, { context });
67
+ });
68
+ chain.chainEnd().on('error', (err, time, context) => {
69
+ state.recordError(err, time, context);
70
+ });
71
+
72
+ this.pool[name] = state;
73
+ this.variablePool[name] = chain;
74
+
75
+ if (chain.chainSupportsPendingChange()) {
76
+ this.priorityPool[name] = chain;
77
+ }
78
+
79
+ this.emit('add-item', state);
80
+ }
81
+
82
+ getVariableNames() {
83
+ const variables = Object.values(this.pool).filter(s => s.isVariable);
84
+ variables.sort((a, b) => a.variableOrder - b.variableOrder);
85
+ return variables.map(v => v.name);
86
+ }
87
+
88
+ getImmediateDependencies(name) {
89
+ const state = this.pool[name];
90
+ if (!state || !state.isVariable) {
91
+ return [];
92
+ }
93
+
94
+ const path = `variables.${state.name}`;
95
+ const names = this.expressionService.referencedNamesByPath[path] ?? [];
96
+ names.sort((a, b) => (this.pool[a]?.id ?? -1) - (this.pool[b]?.id ?? -1));
97
+
98
+ return names;
99
+ }
100
+
101
+ getExtendedDependencies(name) {
102
+ const state = this.pool[name];
103
+ if (!state || !state.isVariable) {
104
+ return [];
105
+ }
106
+
107
+ const found = new Set();
108
+ const explored = new Set();
109
+
110
+ const explore = (name) => {
111
+ explored.add(name);
112
+
113
+ const path = `variables.${name}`;
114
+ const deps = this.expressionService.referencedNamesByPath[path] ?? [];
115
+
116
+ deps.forEach(d => {
117
+ found.add(d);
118
+ if (!explored.has(d)) {
119
+ explore(d);
120
+ }
121
+ });
122
+ };
123
+
124
+ explore(name);
125
+
126
+ const names = Array.from(found);
127
+ names.sort((a, b) => (this.pool[a]?.id ?? -1) - (this.pool[b]?.id ?? -1));
128
+
129
+ return names;
130
+ }
131
+
132
+ /**
133
+ * Inactivates all inputs and variables except for the given identifiers and their extended dependencies
134
+ *
135
+ * @param {*} names
136
+ */
137
+ setFocus(names) {
138
+ const keep = (names ?? []).reduce((accum, name) => {
139
+ this.getExtendedDependencies(name).forEach((dep) => {
140
+ accum.add(dep);
141
+ });
142
+ return accum;
143
+ }, new Set(names));
144
+
145
+ Object.values(this.pool).forEach((state) => {
146
+ state.active = keep.has(state.name);
147
+ });
148
+ }
149
+
150
+ clearFocus() {
151
+ Object.values(this.pool).forEach((state) => {
152
+ state.active = true;
153
+ });
154
+ }
155
+
156
+ currentState() {
157
+ this.stats.currentStateCalls += 1;
158
+ return _.mapValues(this.pool, 'value');
159
+ }
160
+
161
+ getState(name) {
162
+ return this.pool[name];
163
+ }
164
+
165
+ normalizeTime(time) {
166
+ if (this.sources.length > 1 && time < this.lastUpdateTime) {
167
+ time = this.lastUpdateTime;
168
+ } else {
169
+ this.lastUpdateTime = time;
170
+ }
171
+
172
+ return time;
173
+ }
174
+
175
+ updateStateValue(name, value, time) {
176
+ const context = { sourceType: 'source', trigger: name };
177
+
178
+ // Check all variables that might have pending changes occuring before <time>
179
+ this.probe(context, time);
180
+
181
+ // With all potential earlier pending changes resolved, report this state change
182
+ if (this.pool[name]) {
183
+ this.pool[name].update(value, time, { context });
184
+ }
185
+ }
186
+
187
+ updateConditionValue(name, value, time, code, message) {
188
+ const context = { sourceType: 'source', trigger: name };
189
+
190
+ // Check all variables that might have pending changes occuring before <time>
191
+ this.probe(context, time);
192
+
193
+ // With all potential earlier pending changes resolved, report this state change
194
+ if (this.pool[name]) {
195
+ this.pool[name].update(value, time, code, message, { context });
196
+ }
197
+ }
198
+
199
+ probe(context, time) {
200
+ // Check all variables that might have pending changes occuring before <time>
201
+ while (true) { // eslint-disable-line no-constant-condition
202
+ const [chain, pendingTime] = this.getNextPendingVariable(time);
203
+ if (chain === null) {
204
+ break;
205
+ }
206
+
207
+ // A probe call "refreshes" the chain at the specified time with the last value it was updated with.
208
+ // This may or may not result in a value change (and thus a recursive updateStateValue call)
209
+ // Excessively deep call stacks should hopefully be tempered by these recursive calls having earlier timestamps
210
+ // and thus fewer candidate pending changes to further call into.
211
+ chain.probeOrForward(context, pendingTime);
212
+ }
213
+ }
214
+
215
+ // Find the variable chain with the earliest pending change that is scheduled before the given time
216
+ getNextPendingVariable(time) {
217
+ let nextChain = null;
218
+ let nextChangeTime = null;
219
+
220
+ _.each(this.priorityPool, (chain) => {
221
+ const changeTime = chain.chainNextPendingChange(time);
222
+ if (changeTime === null || changeTime >= time) {
223
+ return;
224
+ }
225
+
226
+ if (nextChangeTime === null || changeTime < nextChangeTime) {
227
+ nextChain = chain;
228
+ nextChangeTime = changeTime;
229
+ }
230
+ });
231
+
232
+ return [nextChain, nextChangeTime];
233
+ }
234
+
235
+ addSource(source) {
236
+ this.sources.push(source);
237
+
238
+ if (source.sourceType === 'value') {
239
+ this.addValueSource(source);
240
+ } else if (source.sourceType === 'mtconnect') {
241
+ this.addMtconnectSource(source);
242
+ } else if (source.sourceType === 'generator') {
243
+ this.addGeneratorSource(source);
244
+ }
245
+ }
246
+
247
+ addGeneratorSource(source) {
248
+ if (!this.pool[source.name]) {
249
+ const state = this.createStandardState(source.name);
250
+ this.pool[source.name] = state;
251
+ this.emit('add-item', state);
252
+ }
253
+
254
+ source.on('update', (value, time) => {
255
+ time = this.normalizeTime(time);
256
+ this.updateStateValue(source.name, value, time);
257
+
258
+ if (this.deviceOutput) {
259
+ this.deviceOutput.add(source.name, value, time);
260
+ }
261
+ });
262
+ }
263
+
264
+ addValueSource(source) {
265
+ _.each(this.expressionService.referencedNames('device'), (name) => {
266
+ if (!this.pool[name]) {
267
+ const state = this.createStandardState(name);
268
+ this.pool[name] = state;
269
+ this.emit('add-item', state);
270
+ }
271
+ });
272
+
273
+ source.on('update', (key, value, time) => {
274
+ time = this.normalizeTime(time);
275
+ this.updateStateValue(key, value, time);
276
+
277
+ if (this.deviceOutput) {
278
+ this.deviceOutput.add(key, value, time);
279
+ }
280
+ });
281
+
282
+ source.on('probe', (time) => {
283
+ time = this.normalizeTime(time);
284
+ const context = { sourceType: 'source', trigger: '[probe]' };
285
+ this.probe(context, time);
286
+ });
287
+
288
+ source.on('unavailable', (time) => {
289
+ time = this.normalizeTime(time);
290
+ const context = { sourceType: 'source', trigger: '[unavailable]' };
291
+ this.probe(context, time);
292
+
293
+ _.each(this.expressionService.referencedNames('device'), (name) => {
294
+ if (this.pool[name]) {
295
+ this.pool[name].setUnavailable(time, { context });
296
+ }
297
+
298
+ if (this.deviceOutput) {
299
+ this.deviceOutput.add(name, 'UNAVAILABLE', time);
300
+ }
301
+ });
302
+ });
303
+
304
+ source.on('add-input', (name) => {
305
+ if (!this.pool[name]) {
306
+ const state = this.createStandardState(name);
307
+ this.pool[name] = state;
308
+ this.emit('add-item', state);
309
+ }
310
+ });
311
+
312
+ // Some devices may support 'subscribing' to specfic engine variables by implementing two methods
313
+ if (source.getTrackedVariables && source.updateVariable) {
314
+ _.each(source.getTrackedVariables(), (name) => {
315
+ if (this.pool[name]) {
316
+ this.pool[name].on('update', (value, time) => {
317
+ source.updateVariable(name, value, time);
318
+ });
319
+ }
320
+ });
321
+ }
322
+
323
+ source.on('update-condition', (key, fields, time) => {
324
+ this.emit('update-condition', key, fields, time);
325
+ });
326
+ }
327
+
328
+ addMtconnectSource(source) {
329
+ let mkeys = {};
330
+ if (source.mtconnectKeys) {
331
+ mkeys = _.keyBy(source.mtconnectKeys(), 'name');
332
+ }
333
+
334
+ const names = new Set([...this.expressionService.referencedNames('device'), ...Object.keys(mkeys)]);
335
+ _.each(Array.from(names), (name) => {
336
+ if (!this.pool[name]) {
337
+ let state = null;
338
+ if (mkeys[name]?.type === 'condition') {
339
+ state = this.createConditionState(name);
340
+ } else {
341
+ state = this.createStandardState(name);
342
+ }
343
+
344
+ this.pool[name] = state;
345
+ this.emit('add-item', state);
346
+ }
347
+ });
348
+
349
+ _.each(mkeys, (msource) => {
350
+ let func = null;
351
+ if (msource.type === 'value') {
352
+ func = (value, time) => {
353
+ this.updateStateValue(msource.name, value, time);
354
+ };
355
+ } else if (msource.type === 'condition') {
356
+ func = (value, time) => {
357
+ this.updateConditionValue(msource.name, value.level, time, value.code, value.message);
358
+ };
359
+ }
360
+
361
+ if (func) {
362
+ if (!this.updateFuncs[msource.name]) {
363
+ this.updateFuncs[msource.name] = [];
364
+ }
365
+
366
+ this.updateFuncs[msource.name].push(func);
367
+ }
368
+ });
369
+
370
+ const poolUpdate = (key, updateFunc, _time) => {
371
+ if (!this.pool[key]) {
372
+ const state = this.createStandardState(key);
373
+ this.pool[key] = state;
374
+ this.emit('add-item', state);
375
+ }
376
+
377
+ updateFunc();
378
+ };
379
+
380
+ // valueOf is available on native Date, Moment, and Luxon objects to get milli epoch timestamp
381
+ // valueOf on a number passes back the number
382
+ const parseTimestamp = (time) => {
383
+ return time?.valueOf();
384
+ };
385
+
386
+ // Non-passthrough update function only pumps the engine's variable pool
387
+ source.on('update', (key, value, time) => {
388
+ const poolTime = this.normalizeTime(parseTimestamp(time) / 1000.0);
389
+ time = poolTime * 1000.0;
390
+
391
+ if (this.updateFuncs[key]) {
392
+ _.each(this.updateFuncs[key], func => poolUpdate(key, () => func(value, poolTime), poolTime));
393
+ } else {
394
+ poolUpdate(key, () => {
395
+ if (this.pool[key] instanceof ConditionState) {
396
+ this.updateConditionValue(key, value.level, time, value.code, value.message);
397
+ } else {
398
+ this.updateStateValue(key, value, poolTime);
399
+ }
400
+ }, poolTime);
401
+ }
402
+ });
403
+
404
+ source.on('probe', (time) => {
405
+ const context = { sourceType: 'source', trigger: '[probe]' };
406
+
407
+ time = this.normalizeTime(parseTimestamp(time) / 1000.0);
408
+ this.probe(context, time);
409
+ });
410
+
411
+ source.on('update-sample', (key, value, time) => {
412
+ const poolTime = this.normalizeTime(parseTimestamp(time) / 1000.0);
413
+ time = poolTime * 1000.0;
414
+
415
+ if (this.updateFuncs[key]) {
416
+ _.each(this.updateFuncs[key], func => poolUpdate(key, () => func(value, poolTime), poolTime));
417
+ } else {
418
+ poolUpdate(key, () => this.updateStateValue(key, value, poolTime), poolTime);
419
+ }
420
+
421
+ if (this.deviceOutput) {
422
+ this.deviceOutput.add(key, value, poolTime);
423
+ }
424
+
425
+ this.emit('update-sample', key, value, time);
426
+ });
427
+
428
+ source.on('update-condition', (key, fields, time) => {
429
+ const poolTime = this.normalizeTime(parseTimestamp(time) / 1000.0);
430
+ time = poolTime * 1000.0;
431
+
432
+ if (this.updateFuncs[key]) {
433
+ _.each(this.updateFuncs[key], func => poolUpdate(key, () => func(fields, poolTime), poolTime));
434
+ } else {
435
+ poolUpdate(key, () => this.updateStateValue(key, fields.level || 'NORMAL', poolTime), poolTime);
436
+ }
437
+
438
+ this.emit('update-condition', key, fields, time);
439
+ });
440
+
441
+ source.on('update-message', (key, value, time) => {
442
+ const poolTime = this.normalizeTime(parseTimestamp(time) / 1000.0);
443
+ time = poolTime * 1000.0;
444
+
445
+ if (this.updateFuncs[key]) {
446
+ _.each(this.updateFuncs[key], func => poolUpdate(key, () => func(value, poolTime), poolTime));
447
+ } else {
448
+ poolUpdate(key, () => this.updateStateValue(key, value, poolTime), poolTime);
449
+ }
450
+
451
+ this.emit('update-message', key, value, time);
452
+ });
453
+ }
454
+
455
+ createStandardState(name) {
456
+ const state = new DataState(name);
457
+ this.attachStateHandlers(state);
458
+
459
+ return state;
460
+ }
461
+
462
+ createConditionState(name) {
463
+ const state = new ConditionState(name);
464
+ this.attachStateHandlers(state);
465
+
466
+ return state;
467
+ }
468
+
469
+ attachStateHandlers(state) {
470
+ state.on('update', () => {
471
+ this.emit('update', state.name, state.value, state.valueTime);
472
+ });
473
+ state.on('unavailable', (time) => {
474
+ this.emit('unavailable', state.name, time);
475
+ });
476
+ state.on('cycle-update', () => {
477
+ this.stats.updateCyclesDetected += 1;
478
+ });
479
+ state.on('cycle-unavailable', () => {
480
+ this.stats.unavailCyclesDetected += 1;
481
+ });
482
+ }
483
+ }
484
+
485
+ let stateNextId = 1;
486
+
487
+ class DataState extends EventEmitter {
488
+ constructor(name) {
489
+ super();
490
+ this.id = stateNextId;
491
+ this.name = name;
492
+ this.defaultValue = 0;
493
+ this.value = 0;
494
+ this.valueTime = 0;
495
+ this.updateTime = 0;
496
+ this.changeCount = 0;
497
+ this.updateCount = 0;
498
+ this.available = false;
499
+ this.errorCount = 0;
500
+ this.lastError = null;
501
+ this.lastErrorTime = 0;
502
+ this.lastErrorContext = null;
503
+ this.errorHistory = [];
504
+ this.valueHistory = [];
505
+ this.inUpdate = false;
506
+ this.active = true;
507
+
508
+ stateNextId += 1;
509
+ }
510
+
511
+ // options:
512
+ // forceEmit: Emit update even if value hasn't changed
513
+ // engineUpdate: Whether the normal "update" event should be emitted in addition to "update-unfiltered"
514
+ // Engine updates should occur for variables updated at the end of their op chains or for device values
515
+ // context: Specifies source and name of identifier causing update
516
+
517
+ update(value, time, options = {}) {
518
+ if (value === 'UNAVAILABLE' && !this.isVariable) {
519
+ this.updateInner(value, time, { ...options, engineUpdate: false });
520
+ this.setUnavailable(time, options);
521
+ } else {
522
+ this.updateInner(value, time, { ...options, engineUpdate: !this.isVariable });
523
+ }
524
+ }
525
+
526
+ updateFromChain(value, time, options = {}) {
527
+ this.updateInner(value, time, { ...options, engineUpdate: true });
528
+ }
529
+
530
+ updateInner(value, time, options = {}) {
531
+ if (this.inUpdate) {
532
+ this.recordError('Execution cycle detected on UPDATE', time, options.context);
533
+ this.setUnavailable(time, { ...options, breakCycle: true });
534
+ this.emit('cycle-update', this);
535
+ this.inUpdate = true;
536
+ return;
537
+ }
538
+
539
+ try {
540
+ if (!this.available || !_.isEqual(this.value, value) || options.forceEmit) {
541
+ this.value = value;
542
+ this.valueTime = time;
543
+ this.available = true;
544
+ this.changeCount += 1;
545
+
546
+ if (this.valueHistory.length > 10) {
547
+ this.valueHistory.shift();
548
+ }
549
+
550
+ this.valueHistory.push({
551
+ value: options.historyValue ?? value,
552
+ valueTime: time,
553
+ available: true,
554
+ });
555
+
556
+ if (options.engineUpdate && this.active) {
557
+ this.inUpdate = true;
558
+ this.emit('update', value, time);
559
+ this.inUpdate = false;
560
+ }
561
+ }
562
+
563
+ this.updateTime = time;
564
+ this.updateCount += 1;
565
+
566
+ if (this.active) {
567
+ this.inUpdate = true;
568
+ this.emit('update-unfiltered', value, time, options.context);
569
+ this.inUpdate = false;
570
+ }
571
+ } catch (err) {
572
+ this.recordError(err, time, options.context);
573
+ this.inUpdate = false;
574
+ }
575
+ }
576
+
577
+ setUnavailable(time, options = {}) {
578
+ this.value = this.defaultValue;
579
+ this.valueTime = time;
580
+ this.available = false;
581
+
582
+ if (this.valueHistory.length > 10) {
583
+ this.valueHistory.shift();
584
+ }
585
+ this.valueHistory.push({
586
+ value: this.defaultValue,
587
+ valueTime: time,
588
+ available: false,
589
+ });
590
+
591
+ if (this.inUpdate) {
592
+ if (!options.breakCycle) {
593
+ this.recordError('Execution cycle detected on UNAVAILABLE', time, options.context);
594
+ this.emit('cycle-unavailable', this);
595
+ }
596
+ return;
597
+ }
598
+
599
+ try {
600
+ if (this.active) {
601
+ this.inUpdate = true;
602
+ this.emit('unavailable', time, options.context);
603
+ this.inUpdate = false;
604
+ }
605
+ } catch (err) {
606
+ this.recordError(err, time, options.context);
607
+ this.inUpdate = false;
608
+ }
609
+ }
610
+
611
+ recordError(err, time, context) {
612
+ this.errorCount += 1;
613
+ this.lastError = err;
614
+ this.lastErrorTime = time;
615
+ this.lastErrorContext = context;
616
+
617
+ if (this.errorHistory.length > 10) {
618
+ this.errorHistory.shift();
619
+ }
620
+ this.errorHistory.push({
621
+ error: err,
622
+ errorTime: time,
623
+ errorContext: context,
624
+ });
625
+ }
626
+ }
627
+
628
+ class ConditionState extends DataState {
629
+ constructor(name) {
630
+ super(name);
631
+
632
+ const handler = {
633
+ get: (target, name) => {
634
+ if (target[name]) {
635
+ return target[name];
636
+ }
637
+ return {
638
+ code: null,
639
+ level: this.sharedLevel,
640
+ message: '',
641
+ };
642
+ },
643
+
644
+ set: (target, name, value) => {
645
+ target[name] = value;
646
+ return true;
647
+ },
648
+ };
649
+
650
+ this.sharedLevel = 'UNAVAILABLE';
651
+ this.state = new Proxy({}, handler);
652
+ }
653
+
654
+ update(value, time, code, message, options = {}) {
655
+ const prevSharedLevel = this.sharedLevel;
656
+ if (value === 'UNAVAILABLE') {
657
+ this.sharedLevel = value;
658
+ } else {
659
+ this.sharedLevel = 'NORMAL';
660
+ }
661
+
662
+ message ??= '';
663
+ let forceEmit = false;
664
+ const historyValue = { code, value, message, time };
665
+
666
+ if (code) {
667
+ if (this.state[code]?.code === null) {
668
+ this.state[code] = {
669
+ code,
670
+ level: this.sharedLevel,
671
+ message: '',
672
+ };
673
+ forceEmit = true;
674
+ }
675
+
676
+ if (this.state[code].level !== value || this.state[code].message !== message) {
677
+ this.state[code].code = code;
678
+ this.state[code].level = value;
679
+ this.state[code].updateTime = time;
680
+ this.state[code].message = message;
681
+ forceEmit = true;
682
+ }
683
+
684
+ this.state[code].valueTime = time;
685
+ } else if (value === 'NORMAL' || value === 'UNAVAILABLE') {
686
+ Object.values(this.state).forEach((s) => {
687
+ s.level = value;
688
+ s.message = message;
689
+ s.valueTime = time;
690
+ s.updateTime = time;
691
+ });
692
+
693
+ if (value !== prevSharedLevel) {
694
+ forceEmit = true;
695
+ }
696
+ }
697
+
698
+ super.update(this.state, time, { ...options, forceEmit, historyValue });
699
+ }
700
+
701
+ setUnavailable(time, options = {}) {
702
+ Object.values(this.state).forEach((s) => {
703
+ s.value = 'UNAVAILABLE';
704
+ s.valueTime = time;
705
+ });
706
+
707
+ super.setUnavailable(time, options);
708
+ }
709
+
710
+ getLevel(code) {
711
+ return this.state[code]?.value ?? 'UNAVAILABLE';
712
+ }
713
+
714
+ getMessage(code) {
715
+ return this.state[code]?.message ?? '';
716
+ }
717
+ }
718
+
719
+ module.exports = EngineV2;