@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,186 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const fs = require('fs/promises');
5
+ const path = require('path');
6
+ const EventEmitter = require('eventemitter3');
7
+ const fileUtil = require('../util/fileUtil');
8
+
9
+ class DeviceOutputOptions {
10
+ constructor({ endTime }) {
11
+ this.endTime = endTime;
12
+ }
13
+
14
+ makePath(file) {
15
+ return `${new Date(file.start).toISOString().json}`;
16
+ }
17
+
18
+ async writeFile(filePath, data) {
19
+ await fs.writeFile(filePath, data, { flush: true });
20
+ }
21
+ }
22
+
23
+ class DeviceOutput extends EventEmitter {
24
+ constructor(outputOptions) {
25
+ super();
26
+
27
+ this.outputOptions = outputOptions;
28
+ this.chunkFormat = 'YYYY-MM-DDTHH:mm:ss.SSS[Z]';
29
+ this.chunkInterval = 1000 * 60;
30
+ this.fileInterval = 10;
31
+ this.endTime = outputOptions?.endTime;
32
+
33
+ this.currentChunk = this.startChunk();
34
+ this.chunks = [this.currentChunk];
35
+ }
36
+
37
+ startFile(time) {
38
+ if (this.currentFile) {
39
+ this.endCurrentFile();
40
+ }
41
+
42
+ if (this.endTime && time > this.endTime) {
43
+ return;
44
+ }
45
+
46
+ const datetime = new Date(time);
47
+ const minuteOffset = datetime.getUTCMinutes() % this.fileInterval;
48
+ const start = time - minuteOffset * 60000 - datetime.getUTCSeconds() * 1000 - datetime.getUTCMilliseconds();
49
+ const end = start + this.chunkInterval * this.fileInterval;
50
+
51
+ this.currentFile = {
52
+ start,
53
+ end,
54
+ chunks: [],
55
+ };
56
+
57
+ this.startChunk(time);
58
+ }
59
+
60
+ endCurrentFile() {
61
+ if (!this.currentFile) {
62
+ return;
63
+ }
64
+
65
+ this.endCurrentChunk();
66
+
67
+ const filePath = this.outputOptions.makePath(this.currentFile);
68
+ const dirPath = path.dirname(filePath);
69
+ fileUtil.ensureDirectoryExistence(dirPath);
70
+
71
+ const data = JSON.stringify(this.currentFile.chunks);
72
+
73
+ // Async but we don't need to wait on it
74
+ this.outputOptions.writeFile(filePath, data);
75
+
76
+ this.currentFile = null;
77
+ }
78
+
79
+ startChunk(time) {
80
+ if (this.currentChunk) {
81
+ this.endCurrentChunk();
82
+ }
83
+
84
+ if (this.endTime && time > this.endTime) {
85
+ return;
86
+ }
87
+
88
+ const datetime = new Date(time);
89
+ const start = time - datetime.getUTCSeconds() * 1000 - datetime.getUTCMilliseconds();
90
+ const end = start + this.chunkInterval;
91
+
92
+ this.currentChunk = {
93
+ start,
94
+ end,
95
+ keys: {},
96
+ };
97
+
98
+ if (this.currentFile) {
99
+ this.currentFile.chunks.push(this.currentChunk);
100
+ }
101
+ }
102
+
103
+ endCurrentChunk() {
104
+ if (!this.currentChunk) {
105
+ return;
106
+ }
107
+
108
+ // envelope: [offset (ms), min, max, median, mean]
109
+ // NB: Currently taking a dumb mean, but would be more valuable as integral divided by time segment
110
+
111
+ _.each(this.currentChunk.keys, (key) => {
112
+ key.envelope = _.map(key.envelope, (envelope, offset) => {
113
+ return this.summarizeEnvelope(envelope, offset);
114
+ });
115
+ key.envelope = _.compact(key.envelope);
116
+ });
117
+
118
+ // NB: This would be a good spot to export the chunk data to a NATS bus
119
+
120
+ this.currentChunk = null;
121
+ }
122
+
123
+ summarizeEnvelope(values, offset) {
124
+ values = _.filter(values, _.isFinite);
125
+ values = _.sortBy(values);
126
+
127
+ if (_.isEmpty(values)) {
128
+ return null;
129
+ }
130
+
131
+ const min = _.first(values);
132
+ const max = _.last(values);
133
+ const median = values[Math.floor(values.length / 2)];
134
+ const mean = parseFloat(_.mean(values).toFixed(3));
135
+
136
+ return [+offset, min, max, median, mean];
137
+ }
138
+
139
+ add(key, value, time) {
140
+ try {
141
+ time = Math.floor(time * 1000);
142
+ if (!this.currentFile || time >= this.currentFile.end.valueOf()) {
143
+ this.startFile(time);
144
+ }
145
+
146
+ if (!this.currentChunk || time >= this.currentChunk.end.valueOf()) {
147
+ this.startChunk(time);
148
+ }
149
+
150
+ // If no current chunk, then we've exceeded the end time for collection
151
+ if (!this.currentChunk) {
152
+ return;
153
+ }
154
+
155
+ if (!this.currentChunk.keys[key]) {
156
+ this.currentChunk.keys[key] = {
157
+ envelope: {},
158
+ raw: [],
159
+ };
160
+ }
161
+
162
+ const chunkKey = this.currentChunk.keys[key];
163
+ const offset = time - this.currentChunk.start;
164
+
165
+ if (typeof value === 'number') {
166
+ value = parseFloat(value.toFixed(3));
167
+ }
168
+
169
+ chunkKey.raw.push([offset, value]);
170
+
171
+ const envelopeOffset = offset - (offset % 1000);
172
+ if (!chunkKey.envelope[envelopeOffset]) {
173
+ chunkKey.envelope[envelopeOffset] = [];
174
+ }
175
+
176
+ chunkKey.envelope[envelopeOffset].push(value);
177
+ } catch (err) {
178
+ this.emit('error', err);
179
+ }
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ DeviceOutputOptions,
185
+ DeviceOutput,
186
+ };
@@ -0,0 +1,480 @@
1
+ const _ = require('lodash');
2
+ const EventEmitter = require('eventemitter3');
3
+
4
+ class EngineV1 extends EventEmitter {
5
+ constructor(config = {}, options = {}) {
6
+ super();
7
+
8
+ this.logDir = options.logDir || '.';
9
+ this.logger = options.logger || {
10
+ error: () => {},
11
+ warn: () => {},
12
+ info: () => {},
13
+ };
14
+
15
+ this.diagnostic = options.diagnostic || {
16
+ writePin: () => {},
17
+ writeInput: () => {},
18
+ };
19
+
20
+ this.config = config;
21
+ this.port = config.port;
22
+ this.scanInterval = config.device.scanInterval;
23
+ this.pins = config.device.pins;
24
+ this.analogPins = config.device.analogPins;
25
+ this.digitalPins = config.device.digitalPins;
26
+ this.vpins = config.engine.vpins;
27
+ this.vpinsMap = _.groupBy(this.vpins, 'pin');
28
+
29
+ _(this.vpins).filter({ type: 'analog' }).map((vpin) => {
30
+ vpin.handle = this.handleAnalogSample.bind(this);
31
+ vpin.state = this.defaultAnalogState(vpin);
32
+
33
+ return null;
34
+ }).value();
35
+
36
+ _(this.vpins).filter({ type: 'digital' }).map((vpin) => {
37
+ vpin.handle = this.handleDigitalSample.bind(this);
38
+ vpin.state = this.defaultDigitalState();
39
+
40
+ return null;
41
+ }).value();
42
+
43
+ _(this.vpins).filter({ type: 'counter' }).map((vpin) => {
44
+ vpin.handle = this.handleCounterSample.bind(this);
45
+ vpin.state = this.defaultCounterState();
46
+
47
+ return null;
48
+ }).value();
49
+
50
+ // Current state of all vpins as expression scope
51
+ this.expressionInputState = this.defaultInputState();
52
+ }
53
+
54
+ defaultAnalogState(vpin) {
55
+ const filter = ((f) => {
56
+ switch (f) {
57
+ case 'avg': return newSMAFilter(vpin.sampleWindow);
58
+ case 'min': return newMinFilter(vpin.sampleWindow);
59
+ case 'max': return newMaxFilter(vpin.sampleWindow);
60
+ default: return newIdentityFilter();
61
+ }
62
+ })(vpin.sampleFunc);
63
+
64
+ return {
65
+ defaultValue: 0,
66
+ value: 0,
67
+ lastSampledValue: 0,
68
+ lastReportedValue: 0,
69
+ filter,
70
+ lastSampleTime: 0,
71
+ lastEmitTime: 0,
72
+ };
73
+ }
74
+
75
+ defaultDigitalState() {
76
+ return {
77
+ defaultValue: false,
78
+ value: false,
79
+ signal: false,
80
+ lastChangeTime: 0,
81
+ current: {
82
+ value: false,
83
+ lastChangeTime: 0,
84
+ },
85
+ filter: {
86
+ value: false,
87
+ lastChangeTime: 0,
88
+ lastChangeOnTime: 0,
89
+ lastChangeOffTime: 0,
90
+ },
91
+ };
92
+ }
93
+
94
+ defaultCounterState() {
95
+ return {
96
+ defaultValue: 0,
97
+ value: 0,
98
+ signal: false,
99
+ lastChangeTime: 0,
100
+ current: {
101
+ value: false,
102
+ lastChangeTime: 0,
103
+ },
104
+ filter: {
105
+ value: false,
106
+ lastChangeTime: 0,
107
+ },
108
+ };
109
+ }
110
+
111
+ addSource(source) {
112
+ if (source.sourceType === 'value') {
113
+ this.addValueSource(source);
114
+ } else if (source.sourceType === 'mtconnect') {
115
+ this.addMtconnectSource(source);
116
+ }
117
+ }
118
+
119
+ addValueSource(source) {
120
+ const pinMap = _.keyBy(_.range(12), i => `pin-${i}`);
121
+
122
+ source.on('update', (pin, value, time) => {
123
+ if (_.has(pinMap, pin)) {
124
+ _.each(this.vpinsMap[pinMap[pin]], (vpin) => {
125
+ vpin.handle(vpin, value, time);
126
+ });
127
+ }
128
+ });
129
+
130
+ source.on('unavailable', (time) => {
131
+ _.each(this.vpins, (vpin) => {
132
+ this.emit('unavailable', vpin.name, time);
133
+
134
+ if (vpin.type === 'analog') {
135
+ vpin.state = this.defaultAnalogState(vpin);
136
+ } else if (vpin.type === 'digital') {
137
+ vpin.state = this.defaultDigitalState();
138
+ } else if (vpin.type === 'counter') {
139
+ vpin.state = this.defaultCounterState();
140
+ }
141
+ });
142
+ });
143
+
144
+ source.on('available', (time) => {
145
+ _.each(this.vpins, (vpin) => {
146
+ this.emit('update', vpin.name, vpin.state.value, time);
147
+ });
148
+ });
149
+ }
150
+
151
+ addMtconnectSource(source) {
152
+ source.on('update-sample', (key, value, time) => {
153
+ this.emit('update-sample', key, value, time);
154
+ });
155
+
156
+ source.on('update-condition', (key, fields, time) => {
157
+ this.emit('update-condition', key, fields, time);
158
+ });
159
+
160
+ source.on('update-message', (key, value, time) => {
161
+ this.emit('update-message', key, value, time);
162
+ });
163
+ }
164
+
165
+ inputNames() {
166
+ return _.keys(this.vpins);
167
+ }
168
+
169
+ defaultInputState() {
170
+ return _.mapValues(this.vpins, vpin => vpin.state.defaultValue);
171
+ }
172
+
173
+ currentState() {
174
+ return _.mapValues(this.vpins, vpin => vpin.state.value);
175
+ }
176
+
177
+ setVpinState(vpin, value) {
178
+ vpin.state.value = value;
179
+ this.expressionInputState[vpin.name] = value;
180
+ }
181
+
182
+ sendUpdate(vpin, value, time) {
183
+ if (_.isNumber(value)) {
184
+ value = Math.round(value * 1000) / 1000;
185
+ }
186
+
187
+ if (vpin.log) {
188
+ this.logSample(vpin.log, value, time);
189
+ }
190
+ if (vpin.verboseEmit) {
191
+ this.logger.info(`${vpin.name} emit: ${value} at ${time}`);
192
+ }
193
+
194
+ // this.diagnostic.writeInput(this.port, vpin.name, time, value);
195
+ this.emit('update', vpin.name, value, time);
196
+ }
197
+
198
+ logSample(log, value, time) {
199
+ if (_.isNumber(value)) {
200
+ value = value.toFixed(3);
201
+ }
202
+ log.write(`${time.toFixed(4)}\t${value}\n`);
203
+ }
204
+
205
+ handleAnalogSample(vpin, sampleValue, sampleTime) {
206
+ const prevSampledValue = vpin.state.filter.value();
207
+ const prevSampledTime = sampleTime - this.scanInterval;
208
+
209
+ vpin.state.filter.addSample(sampleValue);
210
+ vpin.state.lastSampledValue = prevSampledValue;
211
+
212
+ if (vpin.state.lastSampleTime + vpin.sampleInterval > sampleTime) {
213
+ return;
214
+ }
215
+
216
+ vpin.state.lastSampleTime = sampleTime;
217
+ this.setVpinState(vpin, vpin.state.filter.value());
218
+
219
+ if (Math.abs(vpin.state.lastReportedValue - vpin.state.value) >= vpin.minDelta) {
220
+ if (vpin.minDelta && vpin.lastEmitTime < prevSampledTime) {
221
+ this.sendUpdate(vpin, vpin.state.lastReportedValue, prevSampledTime);
222
+ }
223
+
224
+ vpin.state.lastReportedValue = vpin.state.value;
225
+ vpin.lastEmitTime = sampleTime;
226
+ this.sendUpdate(vpin, vpin.state.value, sampleTime);
227
+ }
228
+ }
229
+
230
+ handleDigitalSample(vpin, sampleValue, sampleTime) {
231
+ let value = sampleValue;
232
+ if (this.pins[vpin.pinName].mode === 'analog') {
233
+ value = sampleValue >= vpin.vThresh;
234
+ } else if (this.pins[vpin.pinName].mode === 'counter') {
235
+ if (vpin.state.lastCounterValue === undefined) {
236
+ vpin.state.lastCounterValue = sampleValue;
237
+ }
238
+ value = (vpin.state.lastCounterValue !== sampleValue);
239
+ vpin.state.lastCounterValue = sampleValue;
240
+ }
241
+
242
+ if (vpin.invert) {
243
+ value = !value;
244
+ }
245
+
246
+ if (!this.handleDigitalCommon(vpin, value, sampleTime)) {
247
+ return;
248
+ }
249
+
250
+ if (vpin.onLatch && vpin.state.filter.lastChangeOnTime > 0) {
251
+ const onLatch = vpin.onLatch - vpin.filter;
252
+ const insideHoldWindow = vpin.state.filter.lastChangeOnTime + onLatch > sampleTime;
253
+ value = insideHoldWindow;
254
+ }
255
+
256
+ if (vpin.offLatch && vpin.state.filter.lastChangeOffTime > 0) {
257
+ const offLatch = vpin.offLatch - vpin.filter;
258
+ const insideHoldWindow = vpin.state.filter.lastChangeOffTime + offLatch > sampleTime;
259
+ value = !insideHoldWindow;
260
+ }
261
+
262
+ // "reported" value after any filtering or transformation has been applied
263
+ if (value !== vpin.state.signal) {
264
+ vpin.state.signal = value;
265
+ this.setVpinState(vpin, value);
266
+ vpin.state.lastChangeTime = sampleTime;
267
+
268
+ this.sendUpdate(vpin, vpin.state.value, sampleTime);
269
+ }
270
+ }
271
+
272
+ handleCounterSample(vpin, sampleValue, sampleTime) {
273
+ let value = sampleValue;
274
+
275
+ if (this.pins[vpin.pinName].mode === 'counter') {
276
+ if (vpin.sourceType === 'digital') {
277
+ if (vpin.state.lastCounterValue === undefined) {
278
+ vpin.state.lastCounterValue = sampleValue;
279
+ }
280
+ value = (vpin.state.lastCounterValue !== sampleValue);
281
+ vpin.state.lastCounterValue = sampleValue;
282
+ } else {
283
+ if (value !== vpin.state.value) {
284
+ this.setVpinState(vpin, value);
285
+ this.sendUpdate(vpin, vpin.state.value, sampleTime);
286
+ }
287
+ return;
288
+ }
289
+ } else if (this.pins[vpin.pinName].mode === 'analog') {
290
+ value = sampleValue >= vpin.vThresh;
291
+ }
292
+
293
+ if (vpin.invert) {
294
+ value = !value;
295
+ }
296
+
297
+ if (!this.handleDigitalCommon(vpin, value, sampleTime)) {
298
+ return;
299
+ }
300
+
301
+ if (vpin.onLatch && vpin.state.filter.lastChangeOnTime > 0) {
302
+ const onLatch = vpin.onLatch - vpin.filter;
303
+ const insideHoldWindow = vpin.state.filter.lastChangeOnTime + onLatch > sampleTime;
304
+ value = insideHoldWindow;
305
+ }
306
+
307
+ if (vpin.offLatch && vpin.state.filter.lastChangeOffTime > 0) {
308
+ const offLatch = vpin.offLatch - vpin.filter;
309
+ const insideHoldWindow = vpin.state.filter.lastChangeOffTime + offLatch > sampleTime;
310
+ value = !insideHoldWindow;
311
+ }
312
+
313
+ if (value !== vpin.state.signal) {
314
+ const pulseWidth = sampleTime - vpin.state.lastChangeTime;
315
+
316
+ vpin.state.signal = value;
317
+ vpin.state.lastChangeTime = sampleTime;
318
+
319
+ if (vpin.minPulseWidth && pulseWidth < vpin.minPulseWidth) {
320
+ return;
321
+ }
322
+ if (vpin.maxPulseWidth && pulseWidth > vpin.maxPulseWidth) {
323
+ return;
324
+ }
325
+
326
+ if (vpin.suspend) {
327
+ try {
328
+ if (vpin.suspend.evaluate(this.expressionInputState)) {
329
+ return;
330
+ }
331
+ } catch (err) {
332
+ err.extra = {
333
+ port: this.port,
334
+ suspendExpression: vpin.suspendDefinition,
335
+ suspendScope: this.expressionInputState,
336
+ };
337
+ this.logger.error(err);
338
+ }
339
+ }
340
+
341
+ if ((value && vpin.risingEdge) || (!value && vpin.fallingEdge)) {
342
+ this.setVpinState(vpin, vpin.state.value + 1);
343
+ this.sendUpdate(vpin, vpin.state.value, sampleTime);
344
+ }
345
+ }
346
+ }
347
+
348
+ handleDigitalCommon(vpin, value, sampleTime) {
349
+ // "current" value always reflects the latest v-thresh state regardless of applied filters
350
+ if (value !== vpin.state.current.value) {
351
+ vpin.state.current.value = value;
352
+ vpin.state.current.lastChangeTime = sampleTime;
353
+ }
354
+
355
+ // filter: Ignore F->T and T->F transitions if state not held for at least filter seconds
356
+ if (vpin.filter) {
357
+ const insideFilterWindow = vpin.state.current.lastChangeTime + vpin.filter > sampleTime;
358
+ if (insideFilterWindow && !vpin.state.filter.value && vpin.state.current.value) {
359
+ // console.log(`filtered at ${sampleTime}`);
360
+ return false;
361
+ }
362
+ if (insideFilterWindow && vpin.state.filter.value && !vpin.state.current.value) {
363
+ // console.log(`filtered at ${sampleTime}`);
364
+ return false;
365
+ }
366
+ }
367
+
368
+ if (value !== vpin.state.filter.value) {
369
+ vpin.state.filter.value = value;
370
+ vpin.state.filter.lastChangeTime = sampleTime;
371
+ if (value) {
372
+ vpin.state.filter.lastChangeOnTime = sampleTime;
373
+ } else {
374
+ vpin.state.filter.lastChangeOffTime = sampleTime;
375
+ }
376
+ }
377
+
378
+ // on-delay: Ignore F->T transitions if T state has not been held for at least on-delay seconds
379
+ if (vpin.onDelay) {
380
+ const onDelay = vpin.onDelay - vpin.filter;
381
+ const insideOnDelayWindow = vpin.state.filter.lastChangeTime + onDelay > sampleTime;
382
+ if (insideOnDelayWindow && !vpin.state.signal && vpin.state.filter.value) {
383
+ return false;
384
+ }
385
+ }
386
+
387
+ // off-delay: Ignore T->F transitions if F state has not been held for at least off-delay seconds
388
+ if (vpin.offDelay) {
389
+ const offDelay = vpin.offDelay - vpin.filter;
390
+ const insideOffDelayWindow = vpin.state.filter.lastChangeTime + offDelay > sampleTime;
391
+ if (insideOffDelayWindow && vpin.state.signal && !vpin.state.filter.value) {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ return true;
397
+ }
398
+ }
399
+
400
+ function newIdentityFilter() {
401
+ let sample = 0;
402
+
403
+ return {
404
+ addSample: (value) => {
405
+ sample = value;
406
+ },
407
+ value: () => sample,
408
+ };
409
+ }
410
+
411
+ function newSMAFilter(size) {
412
+ const ringBuffer = newRingBuffer(size);
413
+ let sma = 0;
414
+
415
+ return {
416
+ addSample: (value) => {
417
+ value /= size;
418
+ sma += value;
419
+ sma -= ringBuffer.get(ringBuffer.getTailIndex());
420
+ ringBuffer.push(value);
421
+ },
422
+ value: () => sma,
423
+ };
424
+ }
425
+
426
+ function newMinFilter(size) {
427
+ const ringBuffer = newRingBuffer(size);
428
+ let min = 0;
429
+
430
+ return {
431
+ addSample: (value) => {
432
+ const oldest = ringBuffer.get(ringBuffer.getTailIndex());
433
+ ringBuffer.push(value);
434
+
435
+ if (min === oldest) {
436
+ min = _.min(ringBuffer.values());
437
+ } else if (min > value) {
438
+ min = value;
439
+ }
440
+ },
441
+ value: () => min,
442
+ };
443
+ }
444
+
445
+ function newMaxFilter(size) {
446
+ const ringBuffer = newRingBuffer(size);
447
+ let max = 0;
448
+
449
+ return {
450
+ addSample: (value) => {
451
+ const oldest = ringBuffer.get(ringBuffer.getTailIndex());
452
+ ringBuffer.push(value);
453
+
454
+ if (max === oldest) {
455
+ max = _.max(ringBuffer.values());
456
+ } else if (max < value) {
457
+ max = value;
458
+ }
459
+ },
460
+ value: () => max,
461
+ };
462
+ }
463
+
464
+ function newRingBuffer(size) {
465
+ const buffer = _.times(size, _.constant(0));
466
+ let pointer = 0;
467
+
468
+ return {
469
+ get: key => buffer[key % size],
470
+ getHeadIndex: () => (pointer + (size - 1)) % size,
471
+ getTailIndex: () => pointer,
472
+ push: (value) => {
473
+ buffer[pointer] = value;
474
+ pointer = (size + pointer + 1) % size;
475
+ },
476
+ values: () => buffer,
477
+ };
478
+ }
479
+
480
+ module.exports = EngineV1;