@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.
- package/.circleci/config.yml +141 -0
- package/.eslintrc.json +36 -0
- package/.gitattributes +12 -0
- package/CHANGELOG.md +544 -0
- package/README.md +2 -0
- package/index.js +17 -0
- package/lib/config/adapterConfig.js +535 -0
- package/lib/config/common/allowDenyList.js +58 -0
- package/lib/config/common/jsonPath.js +44 -0
- package/lib/config/common/labjackU3T4Common.js +227 -0
- package/lib/config/common/validations.js +142 -0
- package/lib/config/configError.js +32 -0
- package/lib/config/device/adam-6052Config.js +103 -0
- package/lib/config/device/brotherHTTPConfig.js +215 -0
- package/lib/config/device/ethernetIPConfig.js +191 -0
- package/lib/config/device/ifmIotConfig.js +245 -0
- package/lib/config/device/jsonHttpConfig.js +76 -0
- package/lib/config/device/labjackT4Config.js +39 -0
- package/lib/config/device/labjackT7Config.js +192 -0
- package/lib/config/device/labjackU3Config.js +32 -0
- package/lib/config/device/modbusTcpConfig.js +336 -0
- package/lib/config/device/mqttBaseConfig.js +70 -0
- package/lib/config/device/mqttConfig.js +113 -0
- package/lib/config/device/mqttLincolnConfig.js +62 -0
- package/lib/config/device/mtconnectAdapterConfig.js +42 -0
- package/lib/config/device/mtconnectBaseConfig.js +136 -0
- package/lib/config/device/mtconnectConfig.js +52 -0
- package/lib/config/device/mtconnectHaasConfig.js +15 -0
- package/lib/config/device/nullConfig.js +81 -0
- package/lib/config/device/opcuaConfig.js +205 -0
- package/lib/config/device/pcccConfig.js +88 -0
- package/lib/config/engineConfigV1.js +382 -0
- package/lib/config/engineConfigV2.js +106 -0
- package/lib/config/generator/counterGenConfig.js +15 -0
- package/lib/config/generator/cronGenConfig.js +33 -0
- package/lib/config/generator/dateTimeGenConfig.js +33 -0
- package/lib/config/index.js +339 -0
- package/lib/config/transformConfigUtil.js +296 -0
- package/lib/engine/dataOutput.js +357 -0
- package/lib/engine/deviceOutput.js +186 -0
- package/lib/engine/engineV1.js +480 -0
- package/lib/engine/engineV2.js +719 -0
- package/lib/engine/index.js +34 -0
- package/lib/engine/transformBuilderV1.js +111 -0
- package/lib/engine/transformBuilderV2.js +74 -0
- package/lib/expressionService.js +330 -0
- package/lib/math.js +142 -0
- package/lib/transform/accumulate.js +98 -0
- package/lib/transform/average.js +56 -0
- package/lib/transform/debounce.js +152 -0
- package/lib/transform/downsample.js +69 -0
- package/lib/transform/edge.js +34 -0
- package/lib/transform/expression.js +91 -0
- package/lib/transform/fallingEdge.js +70 -0
- package/lib/transform/fromBuffer.js +89 -0
- package/lib/transform/hash.js +41 -0
- package/lib/transform/ignoreValue.js +118 -0
- package/lib/transform/index.js +93 -0
- package/lib/transform/invert.js +25 -0
- package/lib/transform/latch.js +99 -0
- package/lib/transform/latchValue.js +115 -0
- package/lib/transform/logicAnd.js +67 -0
- package/lib/transform/logicOr.js +67 -0
- package/lib/transform/map.js +115 -0
- package/lib/transform/max.js +57 -0
- package/lib/transform/maxLength.js +39 -0
- package/lib/transform/min.js +57 -0
- package/lib/transform/minDelta.js +40 -0
- package/lib/transform/offDelay.js +89 -0
- package/lib/transform/onDelay.js +89 -0
- package/lib/transform/patternEscape.js +24 -0
- package/lib/transform/patternMatch.js +194 -0
- package/lib/transform/patternReplace.js +170 -0
- package/lib/transform/patternTest.js +85 -0
- package/lib/transform/rateOfChange.js +56 -0
- package/lib/transform/reject.js +115 -0
- package/lib/transform/resample.js +96 -0
- package/lib/transform/risingEdge.js +90 -0
- package/lib/transform/risingEdgeCounter.js +179 -0
- package/lib/transform/sampleInterval.js +12 -0
- package/lib/transform/source.js +116 -0
- package/lib/transform/state.js +201 -0
- package/lib/transform/threshold.js +52 -0
- package/lib/transform/toBuffer.js +159 -0
- package/lib/transform/toggle.js +118 -0
- package/lib/transform/transformState.js +193 -0
- package/lib/transform/trim.js +27 -0
- package/lib/transform/util/chainSource.js +96 -0
- package/lib/transform/util/ringBuffer.js +34 -0
- package/lib/transform/valueChange.js +34 -0
- package/lib/transform/valueDecrease.js +38 -0
- package/lib/transform/valueIncrease.js +38 -0
- package/lib/transform/valueIncreaseDiff.js +66 -0
- package/lib/transform/whenUnavailable.js +73 -0
- package/lib/transform/windowCount.js +86 -0
- package/lib/util/fileUtil.js +44 -0
- package/package.json +38 -0
- package/test/.eslintrc.json +15 -0
- package/test/chainedTransform.test.js +88 -0
- package/test/conditions.test.js +118 -0
- package/test/config/ab-pccc.test.js +41 -0
- package/test/config/adam-6052.test.js +16 -0
- package/test/config/adapter.test.js +109 -0
- package/test/config/brother-http.test.js +19 -0
- package/test/config/ethernet-ip.test.js +19 -0
- package/test/config/ifm-iot.test.js +20 -0
- package/test/config/json-http.test.js +47 -0
- package/test/config/labjack-t4.test.js +78 -0
- package/test/config/labjack-t7.test.js +18 -0
- package/test/config/labjack-u3.test.js +17 -0
- package/test/config/modbusTcp.test.js +87 -0
- package/test/config/mqtt.test.js +19 -0
- package/test/config/mqttLincoln.test.js +29 -0
- package/test/config/mtconnect.test.js +63 -0
- package/test/config/mtconnectAdapter.test.js +124 -0
- package/test/config/mtconnectHaas.test.js +15 -0
- package/test/config/null.test.js +16 -0
- package/test/config/opcua.test.js +97 -0
- package/test/config-tests.js +102 -0
- package/test/configFiles/conditions.yml +37 -0
- package/test/configFiles/data-items-legacy.yml +24 -0
- package/test/configFiles/data-items-shorthand.yml +14 -0
- package/test/configFiles/data-items.yml +12 -0
- package/test/configFiles/device/ab-pccc-default.yml +3 -0
- package/test/configFiles/device/ab-pccc-full.yml +13 -0
- package/test/configFiles/device/adam-6052-default.yml +2 -0
- package/test/configFiles/device/brother-http-default.yml +3 -0
- package/test/configFiles/device/ethernet-ip-default.yml +2 -0
- package/test/configFiles/device/ifm-iot-default.yml +2 -0
- package/test/configFiles/device/json-http-bad-prop.yml +13 -0
- package/test/configFiles/device/json-http-bad-prop2.yml +13 -0
- package/test/configFiles/device/json-http-default.yml +3 -0
- package/test/configFiles/device/json-http-std.yml +13 -0
- package/test/configFiles/device/labjack-t4-condition-exprs.yaml +46 -0
- package/test/configFiles/device/labjack-t4-default.yml +3 -0
- package/test/configFiles/device/labjack-t4-pins-alt.yaml +41 -0
- package/test/configFiles/device/labjack-t4-pins.yaml +40 -0
- package/test/configFiles/device/labjack-t4-v1.yaml +29 -0
- package/test/configFiles/device/labjack-t7-default.yml +2 -0
- package/test/configFiles/device/labjack-u3-default.yml +2 -0
- package/test/configFiles/device/modbus-partial.yml +4 -0
- package/test/configFiles/device/modbus-std-extended.yaml +23 -0
- package/test/configFiles/device/modbus-std.yaml +34 -0
- package/test/configFiles/device/modbus-tcp-default.yml +3 -0
- package/test/configFiles/device/mqtt-default.yml +2 -0
- package/test/configFiles/device/mqtt-lincoln-default.yml +3 -0
- package/test/configFiles/device/mqtt-lincoln-full.yml +7 -0
- package/test/configFiles/device/mtconnect-adapter-default.yml +3 -0
- package/test/configFiles/device/mtconnect-adapter-keys.yaml +18 -0
- package/test/configFiles/device/mtconnect-complex-keys.yaml +17 -0
- package/test/configFiles/device/mtconnect-default.yml +3 -0
- package/test/configFiles/device/mtconnect-duplicate-allow-keys.yaml +10 -0
- package/test/configFiles/device/mtconnect-duplicate-declare-keys.yaml +8 -0
- package/test/configFiles/device/mtconnect-duplicate-deny-keys.yaml +10 -0
- package/test/configFiles/device/mtconnect-haas-default.yml +3 -0
- package/test/configFiles/device/mtconnect-std.yaml +18 -0
- package/test/configFiles/device/null-default.yml +1 -0
- package/test/configFiles/device/opcua-bad-tag.yml +18 -0
- package/test/configFiles/device/opcua-bad-tag2.yml +18 -0
- package/test/configFiles/device/opcua-default.yml +3 -0
- package/test/configFiles/device/opcua-std.yml +18 -0
- package/test/configFiles/dump-test.yml +11 -0
- package/test/configFiles/expressionCond.yml +46 -0
- package/test/configFiles/min-config-t4.yaml +4 -0
- package/test/configFiles/min-config-u3.yaml +3 -0
- package/test/configFiles/missing-device.yaml +2 -0
- package/test/configFiles/parse-error1.yml +9 -0
- package/test/configFiles/parse-error2.yml +9 -0
- package/test/configFiles/repro/buffer-convert-repro.yml +15 -0
- package/test/configFiles/repro/chained-delay-timing-repro.yml +13 -0
- package/test/configFiles/repro/count-init-repro.yml +45 -0
- package/test/configFiles/repro/cycle-break-repro.yml +44 -0
- package/test/configFiles/repro/debounce-repro.yml +46 -0
- package/test/configFiles/repro/diff-count-repro.yml +34 -0
- package/test/configFiles/repro/engine-hang-repro.yml +9 -0
- package/test/configFiles/repro/latch-apm-repro.yml +26 -0
- package/test/configFiles/repro/lockout-count-repro.yml +33 -0
- package/test/configFiles/repro/program-extract-repro.yml +38 -0
- package/test/configFiles/repro/state-latch-repro.yml +47 -0
- package/test/configFiles/repro/ternary-repro.yml +26 -0
- package/test/configFiles/transform/debounce.yml +12 -0
- package/test/configFiles/transform/expression.yml +34 -0
- package/test/configFiles/transform/ignoreValue.yml +31 -0
- package/test/configFiles/transform/latch.yml +11 -0
- package/test/configFiles/transform/latchValue.yml +31 -0
- package/test/configFiles/transform/logicAnd.yml +14 -0
- package/test/configFiles/transform/logicOr.yml +14 -0
- package/test/configFiles/transform/map.yml +19 -0
- package/test/configFiles/transform/maxLength.yml +13 -0
- package/test/configFiles/transform/offDelay.yml +12 -0
- package/test/configFiles/transform/pattern-escape.yml +10 -0
- package/test/configFiles/transform/pattern-match.yml +57 -0
- package/test/configFiles/transform/pattern-replace.yml +34 -0
- package/test/configFiles/transform/pattern-test.yml +25 -0
- package/test/configFiles/transform/reject.yml +24 -0
- package/test/configFiles/transform/risingEdgeCounter.yml +36 -0
- package/test/configFiles/transform/source.yml +20 -0
- package/test/configFiles/transform/state.yml +56 -0
- package/test/configFiles/transform/toggle.yml +19 -0
- package/test/configFiles/transform/whenUnavailable.yml +19 -0
- package/test/dataFiles/noisy-pulse.txt +11330 -0
- package/test/dataItems.test.js +140 -0
- package/test/engine-v1-tests.js +418 -0
- package/test/engine-v2-tests.js +284 -0
- package/test/expression-tests.js +171 -0
- package/test/expressionService.test.js +154 -0
- package/test/expressionServiceCondition.test.js +130 -0
- package/test/repro/buffer-convert-repro.test.js +38 -0
- package/test/repro/chained-delay-timing-repro.test.js +34 -0
- package/test/repro/count-init-repro.test.js +46 -0
- package/test/repro/cylce-break-repro.test.js +57 -0
- package/test/repro/debounce-repro.test.js +65 -0
- package/test/repro/diff-count-repro.test.js +79 -0
- package/test/repro/engine-hang-repro.test.js +38 -0
- package/test/repro/latch-apm-repro.test.js +119 -0
- package/test/repro/lockout-count-repro.test.js +84 -0
- package/test/repro/program-extract-repro.test.js +40 -0
- package/test/repro/state-latch-repro.test.js +63 -0
- package/test/repro/ternary-repro.test.js +43 -0
- package/test/transform/accumulte.test.js +18 -0
- package/test/transform/average.test.js +22 -0
- package/test/transform/debounce.test.js +70 -0
- package/test/transform/downsample.test.js +30 -0
- package/test/transform/edge.test.js +27 -0
- package/test/transform/expression.test.js +189 -0
- package/test/transform/fallingEdge.test.js +59 -0
- package/test/transform/fromBuffer.test.js +60 -0
- package/test/transform/hash.test.js +34 -0
- package/test/transform/ignoreValue.test.js +123 -0
- package/test/transform/invert.test.js +26 -0
- package/test/transform/latch.test.js +33 -0
- package/test/transform/latchValue.test.js +126 -0
- package/test/transform/logicAnd.test.js +80 -0
- package/test/transform/logicOr.test.js +80 -0
- package/test/transform/map.test.js +42 -0
- package/test/transform/max.test.js +30 -0
- package/test/transform/maxLength.test.js +32 -0
- package/test/transform/min.test.js +30 -0
- package/test/transform/minDelta.test.js +14 -0
- package/test/transform/offDelay.test.js +123 -0
- package/test/transform/onDelay.test.js +105 -0
- package/test/transform/patternEscape.test.js +18 -0
- package/test/transform/patternMatch.test.js +177 -0
- package/test/transform/patternReplace.test.js +95 -0
- package/test/transform/patternTest.test.js +105 -0
- package/test/transform/rateOfChange.test.js +34 -0
- package/test/transform/reject.test.js +56 -0
- package/test/transform/resample.test.js +193 -0
- package/test/transform/risingEdge.test.js +60 -0
- package/test/transform/risingEdgeCounter.test.js +227 -0
- package/test/transform/sampleInterval.test.js +22 -0
- package/test/transform/source.test.js +137 -0
- package/test/transform/state.test.js +248 -0
- package/test/transform/threshold.test.js +78 -0
- package/test/transform/toBuffer.test.js +60 -0
- package/test/transform/toggle.test.js +92 -0
- package/test/transform/trim.test.js +30 -0
- package/test/transform/valueChange.test.js +14 -0
- package/test/transform/valueDecrease.test.js +32 -0
- package/test/transform/valueIncrease.test.js +32 -0
- package/test/transform/valueIncreaseDiff.test.js +32 -0
- package/test/transform/whenUnavailable.test.js +93 -0
- package/test/transform/windowCount.test.js +26 -0
- package/test/util/testUtils.js +405 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const ConfigError = require('../configError');
|
|
5
|
+
|
|
6
|
+
// Pattern to match valid OPC-UA node ID paths, as supported by node-opcua
|
|
7
|
+
// Our validation may be slightly stricter on some value validation.
|
|
8
|
+
// Other OPC-UA solutions may also support nsu namespaces instead of ns, but node-opcua does not appear to.
|
|
9
|
+
const tagPathPattern = /^((ns=\d+);)?((i=\d+)|(g=[0-9A-Fa-f-]+)|(b=[A-Za-z0-9+/]+={0,2})|(s=[^;]+))$/;
|
|
10
|
+
|
|
11
|
+
// OPCUA supports index range, so accept n or n:m values.
|
|
12
|
+
const tagIndexPattern = /^(\d+)(:(\d+))?$/;
|
|
13
|
+
|
|
14
|
+
class OpcuaConfig {
|
|
15
|
+
constructor(expressionService) {
|
|
16
|
+
this.expressionService = expressionService;
|
|
17
|
+
|
|
18
|
+
_.each(this.getIdentifiers(expressionService.config), name => this.expressionService.addName(name));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getIdentifiers(config = {}) {
|
|
22
|
+
if (_.isEmpty(config.tags)) {
|
|
23
|
+
// No tags, this should be a softer warning
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const keyset = Object.keys(config.tags);
|
|
28
|
+
if (config['opcua-condition-key']) {
|
|
29
|
+
keyset.push(config['opcua-condition-key']);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return _.map(keyset, (name) => {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
channel: 'device',
|
|
36
|
+
defaultValue: 0,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
parse(config = {}) {
|
|
42
|
+
this.endpoint = config.endpoint;
|
|
43
|
+
|
|
44
|
+
this.username = config.username;
|
|
45
|
+
if (!_.isEmpty(this.username)) {
|
|
46
|
+
this.username = this.username.toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.password = config.password;
|
|
50
|
+
if (!_.isEmpty(this.password)) {
|
|
51
|
+
this.password = this.password.toString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.userCertificatePath = config['user-certificate-path'];
|
|
55
|
+
this.userPrivateKeyPath = config['user-private-key-path'];
|
|
56
|
+
|
|
57
|
+
this.tags = this.loadTags(config.tags);
|
|
58
|
+
this.conditionKey = config['opcua-condition-key'];
|
|
59
|
+
|
|
60
|
+
this.securityMode = this.checkSecurityMode(config['security-mode']);
|
|
61
|
+
this.securityPolicy = this.checkSecurityPolicy(config['security-policy']);
|
|
62
|
+
this.readMode = this.checkReadMode(config);
|
|
63
|
+
this.forceReadOnConnect = !!config['force-read-on-connect'];
|
|
64
|
+
this.scanInterval = this.checkScanInterval(config);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
loadTags(tags) {
|
|
68
|
+
// Shorthand syntax: tags: { name: 'path' }
|
|
69
|
+
tags = _.mapValues(tags, (tag, name) => {
|
|
70
|
+
if (_.isString(tag)) {
|
|
71
|
+
// TODO: We'd really like to examine newlines but YAML strips them. Another yaml library could surface this.
|
|
72
|
+
if (/^path\s+/.test(tag)) {
|
|
73
|
+
throw new ConfigError('Malformed tag definition. Possible mix of shorthand and standard form.').atSection('tags').atAttribute(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { path: tag };
|
|
77
|
+
}
|
|
78
|
+
return tag;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return _(tags).map((defn, name) => {
|
|
82
|
+
try {
|
|
83
|
+
const tagPath = this.checkTagPath(defn);
|
|
84
|
+
const { type, property } = defn;
|
|
85
|
+
// let log;
|
|
86
|
+
// if (defn.log) {
|
|
87
|
+
// log = fs.createWriteStream(path.join(this.logDir, defn.log));
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
type,
|
|
93
|
+
property,
|
|
94
|
+
path: tagPath,
|
|
95
|
+
index: this.checkIndex(defn),
|
|
96
|
+
// log,
|
|
97
|
+
verboseSample: !!defn['verbose-sample'],
|
|
98
|
+
verboseEmit: !!defn['verbose-emit'],
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof ConfigError) {
|
|
102
|
+
error.atSection(`tags.${name}`);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}).keyBy('name').value();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
checkTagPath(defn) {
|
|
110
|
+
const path = defn.path;
|
|
111
|
+
if (!path) {
|
|
112
|
+
throw new ConfigError('no path specified');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!tagPathPattern.test(path)) {
|
|
116
|
+
throw new ConfigError(`Invalid node ID path format: ${path}`).atAttribute('path');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
checkIndex(defn) {
|
|
123
|
+
const index = defn.index;
|
|
124
|
+
if (!index) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parts = index.toString().match(tagIndexPattern);
|
|
129
|
+
if (!parts) {
|
|
130
|
+
throw new ConfigError(`Invalid index or index range: ${index}`).atAttribute('index');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const start = +parts[1];
|
|
134
|
+
const end = parts[3] ? +parts[3] : start;
|
|
135
|
+
if (start > end) {
|
|
136
|
+
throw new ConfigError('Index start must be less than end').atAttribute('index');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let indexRange = start;
|
|
140
|
+
if (end > start) {
|
|
141
|
+
indexRange = `${start}:${end}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { start, end, indexRange };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
checkSecurityMode(defn) {
|
|
148
|
+
const securityModes = ['None', 'Sign', 'SignAndEncrypt'];
|
|
149
|
+
if (!defn) {
|
|
150
|
+
return securityModes[0];
|
|
151
|
+
}
|
|
152
|
+
if (!_.includes(securityModes, defn)) {
|
|
153
|
+
throw new ConfigError(`Security Mode not recognized. Valid values are ${securityModes.join(', ')}`).atAttribute('security-mode');
|
|
154
|
+
}
|
|
155
|
+
return defn;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
checkSecurityPolicy(defn) {
|
|
159
|
+
const securityPolicies = [
|
|
160
|
+
'None',
|
|
161
|
+
'Basic128',
|
|
162
|
+
'Basic192',
|
|
163
|
+
'Basic192Rsa15',
|
|
164
|
+
'Basic256Rsa15',
|
|
165
|
+
'Basic256Sha256',
|
|
166
|
+
// new
|
|
167
|
+
'Aes128_Sha256_RsaOaep',
|
|
168
|
+
'PubSub_Aes128_CTR',
|
|
169
|
+
'PubSub_Aes256_CTR',
|
|
170
|
+
// obsoletes
|
|
171
|
+
'Basic128Rsa15',
|
|
172
|
+
'Basic256',
|
|
173
|
+
];
|
|
174
|
+
if (!defn) {
|
|
175
|
+
return securityPolicies[0];
|
|
176
|
+
}
|
|
177
|
+
if (!_.includes(securityPolicies, defn)) {
|
|
178
|
+
throw new ConfigError(`Security Policy not recognized. Valid values are ${securityPolicies.join(', ')}`).atAttribute('security-policy');
|
|
179
|
+
}
|
|
180
|
+
return defn;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
checkReadMode(defn) {
|
|
184
|
+
const mode = defn['read-mode'];
|
|
185
|
+
const readModes = ['subscription', 'polling'];
|
|
186
|
+
if (!mode) {
|
|
187
|
+
return readModes[0];
|
|
188
|
+
}
|
|
189
|
+
if (!_.includes(readModes, mode)) {
|
|
190
|
+
throw new ConfigError(`Read mode not recognized. Valid values are ${readModes.join(', ')}`).atAttribute('read-mode');
|
|
191
|
+
}
|
|
192
|
+
return mode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
checkScanInterval(defn) {
|
|
196
|
+
const scanInterval = +(defn['scan-interval'] || 1.0);
|
|
197
|
+
if (scanInterval <= 0) {
|
|
198
|
+
throw new ConfigError('scan-interval must be > 0').atAttribute('scan-interval');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Math.ceil(scanInterval * 1000);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = OpcuaConfig;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const ConfigError = require('../configError');
|
|
5
|
+
const validations = require('../common/validations');
|
|
6
|
+
|
|
7
|
+
class PcccConfig {
|
|
8
|
+
constructor(expressionService) {
|
|
9
|
+
this.expressionService = expressionService;
|
|
10
|
+
|
|
11
|
+
_.each(this.getIdentifiers(expressionService.config), name => this.expressionService.addName(name));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getIdentifiers(config = {}) {
|
|
15
|
+
return _.map(_.keys(config.tags), (name) => {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
channel: 'device',
|
|
19
|
+
defaultValue: 0,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
parse(config = {}) {
|
|
25
|
+
const { host, port } = validations.checkEndpoint(config, 'endpoint');
|
|
26
|
+
this.host = host;
|
|
27
|
+
this.port = port;
|
|
28
|
+
|
|
29
|
+
this.scanRate = validations.checkScanInterval(config, 'scan-interval');
|
|
30
|
+
this.routing = validations.checkBoolean(config, 'routing');
|
|
31
|
+
this.verboseDevice = validations.checkBoolean(config, 'verbose-device');
|
|
32
|
+
this.extraVerboseDevice = validations.checkBoolean(config, 'extra-verbose-device');
|
|
33
|
+
this.deviceTimeout = validations.checkNumber(config, 'device-timeout', { defaultValue: 4.5, min: 1 });
|
|
34
|
+
this.slot = this.checkSlot(config);
|
|
35
|
+
this.tags = this.loadTags(config.tags);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
loadTags(tags) {
|
|
39
|
+
// Shorthand syntax: tags: { name: 'path' }
|
|
40
|
+
tags = _.mapValues(tags, (tag, name) => {
|
|
41
|
+
if (_.isString(tag)) {
|
|
42
|
+
// TODO: We'd really like to examine newlines but YAML strips them. Another yaml library could surface this.
|
|
43
|
+
if (/^path\s+/.test(tag)) {
|
|
44
|
+
throw new ConfigError('Malformed tag definition. Possible mix of shorthand and standard form.').atSection('tags').atAttribute(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { path: tag };
|
|
48
|
+
}
|
|
49
|
+
return tag;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return _(tags).map((defn, name) => {
|
|
53
|
+
try {
|
|
54
|
+
const tagPath = this.checkTagPath(defn);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
path: tagPath,
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof ConfigError) {
|
|
62
|
+
error.atSection(`tags.${name}`);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}).keyBy('name').value();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
checkSlot(defn) {
|
|
70
|
+
const slot = defn.slot || 0;
|
|
71
|
+
return slot;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
checkTagPath(defn) {
|
|
75
|
+
const path = defn.path;
|
|
76
|
+
if (_.isEmpty(path)) {
|
|
77
|
+
throw new ConfigError('No tag path specified');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!/^([A-Za-z]+)(\d+):(\d+)(?:(?:\/(\d+))|(?:\/(?:DN|EN|TT))|(?:\.(?:ACC|PRE|LEN)))?(?:,(\d+))?$/.test(path)) {
|
|
81
|
+
throw new ConfigError(`Invalid tag path: ${path}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = PcccConfig;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const math = require('../math');
|
|
5
|
+
const ConfigError = require('./configError');
|
|
6
|
+
|
|
7
|
+
class EngineConfig {
|
|
8
|
+
constructor(expressionService) {
|
|
9
|
+
this.expressionService = expressionService;
|
|
10
|
+
|
|
11
|
+
_.each(this.getIdentifiers(expressionService.config), name => expressionService.addName(name));
|
|
12
|
+
_.each(this.getExpressions(expressionService.config), name => expressionService.addExpression(name));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getIdentifiers(config = {}) {
|
|
16
|
+
if (!config.input) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return _.map(_.keys(this.loadInputExpressionNames(config.input || {})), (name) => {
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
channel: 'engine',
|
|
24
|
+
defaultValue: 0,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getExpressions(config = {}) {
|
|
30
|
+
if (!config.input) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const analog = _.pickBy(config.input.analog, defn => _.has(defn, 'pin'));
|
|
35
|
+
const digital = _.pickBy(config.input.digital, defn => _.has(defn, 'pin'));
|
|
36
|
+
const counter = _.pickBy(config.input.counter, defn => _.has(defn, 'pin'));
|
|
37
|
+
const counterSusp = _.pickBy(counter, defn => _.has(defn, 'suspend'));
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
..._.map(analog, (defn, name) => {
|
|
41
|
+
return {
|
|
42
|
+
path: `input.analog.${name}`,
|
|
43
|
+
expression: `pin-${defn.pin}`,
|
|
44
|
+
};
|
|
45
|
+
}),
|
|
46
|
+
..._.map(digital, (defn, name) => {
|
|
47
|
+
return {
|
|
48
|
+
path: `input.digital.${name}`,
|
|
49
|
+
expression: `pin-${defn.pin}`,
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
..._.map(counter, (defn, name) => {
|
|
53
|
+
return {
|
|
54
|
+
path: `input.counter.${name}`,
|
|
55
|
+
expression: `pin-${defn.pin}`,
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
..._.map(counterSusp, (defn, name) => {
|
|
59
|
+
return {
|
|
60
|
+
path: `input.counter.${name}.suspend`,
|
|
61
|
+
expression: defn.suspend,
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
parse(config = {}) {
|
|
68
|
+
const input = config.input || {};
|
|
69
|
+
|
|
70
|
+
this.scanInterval = config['scan-interval'] || 0.002;
|
|
71
|
+
this.deviceSourceType = 'value';
|
|
72
|
+
|
|
73
|
+
this.inputToExpression = this.loadInputExpressionNames(input);
|
|
74
|
+
this.expressionToInput = _.invert(this.inputToExpression);
|
|
75
|
+
|
|
76
|
+
this.vpins = {
|
|
77
|
+
...this.loadAnalogPinInput(input.analog),
|
|
78
|
+
...this.loadDigitalPinInput(input.digital),
|
|
79
|
+
...this.loadCounterPinInput(input.counter),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
loadInputExpressionNames(input) {
|
|
84
|
+
const names = [
|
|
85
|
+
..._.keys(input.analog),
|
|
86
|
+
..._.keys(input.digital),
|
|
87
|
+
..._.keys(input.counter),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return _(names).keyBy(_.identity).mapValues(_.snakeCase).value();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
loadCommonPinInput(item, name) {
|
|
94
|
+
this.checkPin(item);
|
|
95
|
+
this.checkInputName(name);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
pin: item.pin,
|
|
100
|
+
pinName: this.deviceSourceType === 'value' ? `pin-${item.pin}` : item.pin,
|
|
101
|
+
verboseSample: !!item['verbose-sample'],
|
|
102
|
+
verboseEmit: !!item['verbose-emit'],
|
|
103
|
+
logPath: item.log,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loadAnalogPinInput(items) {
|
|
108
|
+
return _(items).map((defn, name) => {
|
|
109
|
+
try {
|
|
110
|
+
const common = this.loadCommonPinInput(defn, name);
|
|
111
|
+
|
|
112
|
+
const minDelta = this.checkMinDelta(defn);
|
|
113
|
+
const sampleInterval = this.checkSampleInterval(defn);
|
|
114
|
+
const sampleFunc = this.checkSampleFunc(defn);
|
|
115
|
+
const sampleWindow = this.checkSampleWindow(defn);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...common,
|
|
119
|
+
type: 'analog',
|
|
120
|
+
sampleInterval,
|
|
121
|
+
sampleFunc,
|
|
122
|
+
sampleWindow,
|
|
123
|
+
minDelta,
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof ConfigError) {
|
|
127
|
+
error.atSection(`input.analog.${name}`);
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}).keyBy('name').value();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
loadDigitalPinInput(items) {
|
|
135
|
+
return _(items).map((defn, name) => {
|
|
136
|
+
try {
|
|
137
|
+
const common = this.loadCommonPinInput(defn, name);
|
|
138
|
+
|
|
139
|
+
const vThresh = this.checkVThresh(defn);
|
|
140
|
+
const onDelay = this.checkOnDelay(defn);
|
|
141
|
+
const offDelay = this.checkOffDelay(defn);
|
|
142
|
+
const filter = this.checkFilter(defn);
|
|
143
|
+
const onLatch = this.checkOnLatch(defn);
|
|
144
|
+
const offLatch = this.checkOffLatch(defn);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
...common,
|
|
148
|
+
type: 'digital',
|
|
149
|
+
vThresh,
|
|
150
|
+
invert: !!defn.invert,
|
|
151
|
+
onDelay,
|
|
152
|
+
offDelay,
|
|
153
|
+
filter,
|
|
154
|
+
onLatch,
|
|
155
|
+
offLatch,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof ConfigError) {
|
|
159
|
+
error.atSection(`input.digital.${name}`);
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}).keyBy('name').value();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
loadCounterPinInput(items) {
|
|
167
|
+
return _(items).map((defn, name) => {
|
|
168
|
+
try {
|
|
169
|
+
const common = this.loadCommonPinInput(defn, name);
|
|
170
|
+
|
|
171
|
+
const vThresh = this.checkVThresh(defn);
|
|
172
|
+
const edge = this.checkEdge(defn);
|
|
173
|
+
const onDelay = this.checkOnDelay(defn);
|
|
174
|
+
const offDelay = this.checkOffDelay(defn);
|
|
175
|
+
const filter = this.checkFilter(defn);
|
|
176
|
+
const minPulseWidth = this.checkMinPulseWidth(defn);
|
|
177
|
+
const maxPulseWidth = this.checkMaxPulseWidth(defn);
|
|
178
|
+
const suspend = this.checkSuspend(defn);
|
|
179
|
+
const sourceType = defn['source-type'];
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...common,
|
|
183
|
+
type: 'counter',
|
|
184
|
+
sourceType,
|
|
185
|
+
vThresh,
|
|
186
|
+
invert: !!defn.invert,
|
|
187
|
+
risingEdge: edge === 'rising' || edge === 'both',
|
|
188
|
+
fallingEdge: edge === 'falling' || edge === 'both',
|
|
189
|
+
onDelay,
|
|
190
|
+
offDelay,
|
|
191
|
+
filter,
|
|
192
|
+
minPulseWidth,
|
|
193
|
+
maxPulseWidth,
|
|
194
|
+
suspend,
|
|
195
|
+
suspendDefinition: defn.suspend,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof ConfigError) {
|
|
199
|
+
error.atSection(`input.counter.${name}`);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}).keyBy('name').value();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
checkInputName(name) {
|
|
207
|
+
if (!name.match(/^[A-Za-z0-9-_]+$/)) {
|
|
208
|
+
throw new ConfigError('input names can only contain alpha-numeric characters, dash, and underscore');
|
|
209
|
+
}
|
|
210
|
+
return name;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
checkPin(defn) {
|
|
214
|
+
const pin = defn.pin;
|
|
215
|
+
if (!_.isFinite(pin)) {
|
|
216
|
+
throw new ConfigError('no pin specified');
|
|
217
|
+
}
|
|
218
|
+
if (pin < 0 || pin > 15) {
|
|
219
|
+
throw new ConfigError('pin must be in range [0-15]').atAttribute('pin');
|
|
220
|
+
}
|
|
221
|
+
return pin;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
checkSampleInterval(defn) {
|
|
225
|
+
const sampleInterval = defn['sample-interval'] || 1;
|
|
226
|
+
if (sampleInterval <= 0) {
|
|
227
|
+
throw new ConfigError('interval must be greater than 0').atAttribute('sample-interval');
|
|
228
|
+
}
|
|
229
|
+
return sampleInterval;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
checkSampleFunc(defn) {
|
|
233
|
+
const sampleFunc = defn['sample-function'];
|
|
234
|
+
if (sampleFunc && !_.includes(['avg', 'min', 'max'], sampleFunc)) {
|
|
235
|
+
throw new ConfigError('valid functions are avg, min, max').atAttribute('sample-function');
|
|
236
|
+
}
|
|
237
|
+
return sampleFunc;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
checkSampleWindow(defn) {
|
|
241
|
+
const sampleFunc = defn['sample-function'];
|
|
242
|
+
const sampleWindow = defn['sample-window'];
|
|
243
|
+
|
|
244
|
+
if (sampleFunc && !sampleWindow) {
|
|
245
|
+
throw new ConfigError('a sample-window must be provided when a sample-function is defined');
|
|
246
|
+
}
|
|
247
|
+
if (sampleWindow <= 0) {
|
|
248
|
+
throw new ConfigError('window size must be greater than 0').atAttribute('sample-window');
|
|
249
|
+
}
|
|
250
|
+
return parseInt(sampleWindow / this.scanInterval, 10);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
checkMinDelta(defn) {
|
|
254
|
+
const minDelta = defn['min-delta'] || 0;
|
|
255
|
+
if (minDelta < 0) {
|
|
256
|
+
throw new ConfigError('value must be 0 or greater').atAttribute('min-delta');
|
|
257
|
+
}
|
|
258
|
+
return minDelta;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
checkVThresh(defn) {
|
|
262
|
+
const vThresh = defn['v-threshold'] || 1.5;
|
|
263
|
+
if (vThresh <= 0 || vThresh >= 10) {
|
|
264
|
+
throw new ConfigError('threshold must be in range (0, 10) volts').atAttribute('v-threshold');
|
|
265
|
+
}
|
|
266
|
+
return vThresh;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
checkOnDelay(defn) {
|
|
270
|
+
const onDelay = defn['on-delay'] || 0;
|
|
271
|
+
const filter = defn.filter || 0;
|
|
272
|
+
if (onDelay < 0) {
|
|
273
|
+
throw new ConfigError('delay must be 0 or greater').atAttribute('on-delay');
|
|
274
|
+
}
|
|
275
|
+
if (onDelay && filter > onDelay) {
|
|
276
|
+
throw new ConfigError('delay must not be less than the specified filter').atAttribute('on-delta');
|
|
277
|
+
}
|
|
278
|
+
return onDelay;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
checkOffDelay(defn) {
|
|
282
|
+
const offDelay = defn['off-delay'] || 0;
|
|
283
|
+
const filter = defn.filter || 0;
|
|
284
|
+
if (offDelay < 0) {
|
|
285
|
+
throw new ConfigError('delay must be 0 or greater').atAttribute('off-delay');
|
|
286
|
+
}
|
|
287
|
+
if (offDelay && filter > offDelay) {
|
|
288
|
+
throw new ConfigError('delay must not be less than the specified filter').atAttribute('off-delay');
|
|
289
|
+
}
|
|
290
|
+
return offDelay;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
checkOnLatch(defn) {
|
|
294
|
+
const onLatch = defn['on-latch'] || 0;
|
|
295
|
+
const offLatch = defn['off-latch'];
|
|
296
|
+
const offDelay = defn['off-delay'];
|
|
297
|
+
const filter = defn.filter || 0;
|
|
298
|
+
|
|
299
|
+
if (onLatch && offLatch) {
|
|
300
|
+
throw new ConfigError('not compatible with off-latch option').atAttribute('on-latch');
|
|
301
|
+
}
|
|
302
|
+
if (onLatch && offDelay) {
|
|
303
|
+
throw new ConfigError('not compatible with off-delay option').atAttribute('on-latch');
|
|
304
|
+
}
|
|
305
|
+
if (onLatch < 0) {
|
|
306
|
+
throw new ConfigError('value must be 0 or greater').atAttribute('on-latch');
|
|
307
|
+
}
|
|
308
|
+
if (onLatch && filter > onLatch) {
|
|
309
|
+
throw new ConfigError('value must not be less than the specified filter').atAttribute('on-latch');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return onLatch;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
checkOffLatch(defn) {
|
|
316
|
+
const offLatch = defn['off-latch'] || 0;
|
|
317
|
+
const onLatch = defn['on-latch'];
|
|
318
|
+
const onDelay = defn['on-delay'];
|
|
319
|
+
const filter = defn.filter || 0;
|
|
320
|
+
|
|
321
|
+
if (offLatch && onLatch) {
|
|
322
|
+
throw new ConfigError('not compatible with on-latch option').atAttribute('off-latch');
|
|
323
|
+
}
|
|
324
|
+
if (offLatch && onDelay) {
|
|
325
|
+
throw new ConfigError('not compatible with on-delay option').atAttribute('off-latch');
|
|
326
|
+
}
|
|
327
|
+
if (offLatch < 0) {
|
|
328
|
+
throw new ConfigError('value must be 0 or greater').atAttribute('off-latch');
|
|
329
|
+
}
|
|
330
|
+
if (offLatch && filter > onLatch) {
|
|
331
|
+
throw new ConfigError('value must not be less than the specified filter').atAttribute('off-latch');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return offLatch;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
checkFilter(defn) {
|
|
338
|
+
const filter = defn.filter || 0;
|
|
339
|
+
if (filter < 0) {
|
|
340
|
+
throw new ConfigError('filter must be 0 or greater').atAttribute('filter');
|
|
341
|
+
}
|
|
342
|
+
return filter;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
checkEdge(defn) {
|
|
346
|
+
const edge = defn['trigger-edge'] || 'rising';
|
|
347
|
+
if (!_.includes(['rising', 'falling', 'both'], edge)) {
|
|
348
|
+
throw new ConfigError('value must be rising, falling, or both').atAttribute('trigger-edge');
|
|
349
|
+
}
|
|
350
|
+
return edge;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
checkMinPulseWidth(defn) {
|
|
354
|
+
const minPulseWidth = defn['min-pulse-width'] || 0;
|
|
355
|
+
if (minPulseWidth < 0) {
|
|
356
|
+
throw new ConfigError('width must be 0 or greater').atAttribute('min-pulse-width');
|
|
357
|
+
}
|
|
358
|
+
return minPulseWidth;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
checkMaxPulseWidth(defn) {
|
|
362
|
+
const maxPulseWidth = defn['max-pulse-width'] || 0;
|
|
363
|
+
if (maxPulseWidth < 0) {
|
|
364
|
+
throw new ConfigError('width must be 0 or greater').atAttribute('max-pulse-width');
|
|
365
|
+
}
|
|
366
|
+
return maxPulseWidth;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
checkSuspend(defn) {
|
|
370
|
+
if (!defn.suspend) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
return math.compile(defn.suspend);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
throw new ConfigError(`Problem evaluating suspend expression: ${err.message}`).atAttribute('suspend');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = EngineConfig;
|