@machinemetrics/io-adapter-lib 2.35.1 → 2.37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.37.0]
4
+ - Brother config changes in support of d00 (c00<->d00 may not be the exact cutoff)
5
+
6
+ ## [2.36.0]
7
+ - Added if / else-if / else branch transform
8
+
3
9
  ## [2.35.1]
4
10
  - Fixed value-increase and value-decrease to not emit true when coming back from UNAVAILABLE
5
11
  - Behavior change: value-decrease does not emit true on start
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const ConfigError = require('./configError');
5
+
6
+ const BRANCH_KEYS = ['if', 'else-if', 'else'];
7
+
8
+ function isBranchKey(key) {
9
+ return BRANCH_KEYS.includes(key);
10
+ }
11
+
12
+ function getBranchKey(item) {
13
+ if (!_.isObject(item) || _.isArray(item)) {
14
+ return null;
15
+ }
16
+ const keys = _.keys(item);
17
+ if (keys.length !== 1) {
18
+ return null;
19
+ }
20
+ const key = keys[0];
21
+ return isBranchKey(key) ? key : null;
22
+ }
23
+
24
+ /**
25
+ * Preprocesses a variable's transform list to collapse consecutive if/else-if/else
26
+ * into a single branch transform. The user-facing YAML syntax allows:
27
+ *
28
+ * - if:
29
+ * - condition: this == 1
30
+ * - op1
31
+ * - else-if:
32
+ * - condition: this == 2
33
+ * - op1
34
+ * - else:
35
+ * - op1
36
+ *
37
+ * This gets rewritten to:
38
+ *
39
+ * - branch:
40
+ * - if: [...]
41
+ * - else-if: [...]
42
+ * - else: [...]
43
+ */
44
+ function preprocessBranchTransforms(transformList, sectionPath) {
45
+ if (!_.isArray(transformList)) {
46
+ return transformList;
47
+ }
48
+
49
+ const result = [];
50
+ let i = 0;
51
+
52
+ while (i < transformList.length) {
53
+ const key = getBranchKey(transformList[i]);
54
+
55
+ if (key !== 'if') {
56
+ result.push(transformList[i]);
57
+ i += 1;
58
+ } else {
59
+ const branchComponents = [];
60
+ let seenElse = false;
61
+
62
+ while (i < transformList.length) {
63
+ const currentKey = getBranchKey(transformList[i]);
64
+
65
+ if (currentKey === null) {
66
+ break;
67
+ }
68
+
69
+ if (currentKey === 'if') {
70
+ if (branchComponents.length > 0) {
71
+ break;
72
+ }
73
+ const item = transformList[i];
74
+ const body = item[currentKey];
75
+ const condition = body[0];
76
+ const chainTransforms = preprocessBranchTransforms(body.slice(1), `${sectionPath}.${i}.if`);
77
+ branchComponents.push({ [currentKey]: [condition, ...chainTransforms] });
78
+ i += 1;
79
+ } else if (currentKey === 'else-if') {
80
+ if (branchComponents.length === 0) {
81
+ throw new ConfigError('else-if must follow if or else-if')
82
+ .atPath(sectionPath);
83
+ }
84
+ if (seenElse) {
85
+ throw new ConfigError('else-if cannot follow else')
86
+ .atPath(sectionPath);
87
+ }
88
+ const item = transformList[i];
89
+ const body = item[currentKey];
90
+ const condition = body[0];
91
+ const chainTransforms = preprocessBranchTransforms(body.slice(1), `${sectionPath}.${i}.else-if`);
92
+ branchComponents.push({ [currentKey]: [condition, ...chainTransforms] });
93
+ i += 1;
94
+ } else if (currentKey === 'else') {
95
+ if (branchComponents.length === 0) {
96
+ throw new ConfigError('else must follow if or else-if')
97
+ .atPath(sectionPath);
98
+ }
99
+ if (seenElse) {
100
+ throw new ConfigError('only one else allowed per branch')
101
+ .atPath(sectionPath);
102
+ }
103
+ seenElse = true;
104
+ const item = transformList[i];
105
+ const body = item[currentKey];
106
+ const chainTransforms = preprocessBranchTransforms(body, `${sectionPath}.${i}.else`);
107
+ branchComponents.push({ [currentKey]: chainTransforms });
108
+ i += 1;
109
+ } else {
110
+ break;
111
+ }
112
+ }
113
+
114
+ result.push({ branch: branchComponents });
115
+ }
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ module.exports = {
122
+ preprocessBranchTransforms,
123
+ };
@@ -16,7 +16,10 @@ class BrotherHTTPConfig {
16
16
  };
17
17
 
18
18
  const mode = expressionService.config['collection-mode'];
19
- const defaultMode = this.DataCollectionModes.MIXED;
19
+ const deviceName = expressionService.config.device;
20
+ const defaultMode = deviceName === 'brother-ftp'
21
+ ? this.DataCollectionModes.FTP
22
+ : this.DataCollectionModes.MIXED;
20
23
  if (mode === undefined) this.dataCollectionMode = defaultMode;
21
24
  else if (mode.toLowerCase() === 'http') this.dataCollectionMode = this.DataCollectionModes.HTTP;
22
25
  else if (mode.toLowerCase() === 'ftp') this.dataCollectionMode = this.DataCollectionModes.FTP;
@@ -141,15 +144,23 @@ class BrotherHTTPConfig {
141
144
 
142
145
  /** Configuration indicates how to collect data:
143
146
  * HTTP only,
144
- * FTP only (default),
147
+ * FTP only (default for device brother-ftp),
145
148
  * HTTP + FTP for program headers
146
149
  */
147
- let mode = config['collection-mode'] ?? '';
148
- if (_.isString(mode)) {
149
- mode = mode.toLowerCase();
150
+ const isBrotherFtp = config.device === 'brother-ftp';
151
+ let mode = config['collection-mode'];
152
+ if (mode === undefined || mode === null || mode === '') {
153
+ mode = isBrotherFtp ? 'ftp' : '';
154
+ }
155
+ if (!_.isString(mode)) {
156
+ throw new ConfigError('collection-mode must be a string').atAttribute('collection-mode');
157
+ }
158
+ mode = mode.toLowerCase();
159
+ if (isBrotherFtp && mode !== 'ftp') {
160
+ throw new ConfigError('device brother-ftp requires collection-mode ftp').atAttribute('collection-mode');
150
161
  }
151
162
  if (!_.includes(['http', 'ftp', 'mixed'], mode)) {
152
- throw new ConfigError('collection-mode must be one of: {http, ftp, mixed}').atAttribute('macros');
163
+ throw new ConfigError('collection-mode must be one of: {http, ftp, mixed}').atAttribute('collection-mode');
153
164
  }
154
165
 
155
166
  let keysThatAlreadyExist = '';
@@ -191,6 +202,36 @@ class BrotherHTTPConfig {
191
202
  throw new ConfigError(`The value of macros key ${key} must be a valid integer`).atAttribute(key);
192
203
  }
193
204
  });
205
+
206
+ this.assignOptionalRelativePath(config, 'memory-path', 'memoryPath');
207
+ this.assignOptionalRelativePath(config, 'production-path', 'productionPath');
208
+ this.assignOptionalRelativePath(config, 'panel-path', 'panelPath');
209
+ this.assignOptionalRelativePath(config, 'position-path', 'positionPath');
210
+ this.assignOptionalRelativePath(config, 'work-counter-path', 'workCounterPath');
211
+ this.assignOptionalRelativePath(config, 'alarm-path', 'alarmPath');
212
+ this.assignOptionalRelativePath(config, 'macro-path', 'macroPath');
213
+
214
+ if (_.has(config, 'cnc-version')) {
215
+ const cncVersion = config['cnc-version'];
216
+ if (cncVersion !== 'legacy' && cncVersion !== 'd00') {
217
+ throw new ConfigError('cnc-version must be one of: legacy, d00').atAttribute('cnc-version');
218
+ }
219
+ this.cncVersion = cncVersion;
220
+ }
221
+ }
222
+
223
+ assignOptionalRelativePath(config, yamlKey, propertyName) {
224
+ if (!_.has(config, yamlKey)) {
225
+ return;
226
+ }
227
+ const value = config[yamlKey];
228
+ if (!_.isString(value) || value.trim() === '') {
229
+ throw new ConfigError(`${yamlKey} must be a non-empty string`).atAttribute(yamlKey);
230
+ }
231
+ if (value.includes('..')) {
232
+ throw new ConfigError(`${yamlKey} must not contain '..'`).atAttribute(yamlKey);
233
+ }
234
+ this[propertyName] = value;
194
235
  }
195
236
 
196
237
  checkScanInterval(config) {
@@ -3,6 +3,7 @@
3
3
  const _ = require('lodash');
4
4
  const ConfigError = require('./configError');
5
5
  const TransformConfigUtil = require('./transformConfigUtil');
6
+ const { preprocessBranchTransforms } = require('./branchPreprocessor');
6
7
 
7
8
  class EngineConfig {
8
9
  constructor(expressionService, config = {}, device = {}) {
@@ -48,6 +49,10 @@ class EngineConfig {
48
49
  return variable;
49
50
  });
50
51
 
52
+ this.config.variables = _.mapValues(this.config.variables, (variable, name) => {
53
+ return preprocessBranchTransforms(variable, `variables.${name}`);
54
+ });
55
+
51
56
  _.each(this.getExpressions(this.config), name => expressionService.addExpression(name));
52
57
  _.each(this.getStringExpressions(this.config), name => expressionService.addStringExpression(name));
53
58
  }
@@ -204,6 +204,7 @@ class Config {
204
204
  return new OPCUAConfig(expressionService);
205
205
  case 'brother':
206
206
  case 'brother-http':
207
+ case 'brother-ftp':
207
208
  return new BrotherHTTPConfig(expressionService);
208
209
  case 'adam-6052':
209
210
  return new AdamConfig(expressionService);
@@ -315,6 +316,7 @@ class Config {
315
316
  'opc-ua',
316
317
  'brother',
317
318
  'brother-http',
319
+ 'brother-ftp',
318
320
  'adam-6052',
319
321
  'mtconnect',
320
322
  'mtconnect-haas',
@@ -259,7 +259,7 @@ class ExpressionService {
259
259
  if (tokens) {
260
260
  const first = tokens[0];
261
261
  if (first === 'this' || this.channelsLookup[first]) {
262
- accum[first] = _.reduceRight(_.tail(tokens), (sub, item) => {
262
+ const nested = _.reduceRight(_.tail(tokens), (sub, item) => {
263
263
  if (_.isNumber(item)) {
264
264
  return [
265
265
  ..._.times(Math.max(0, item - 1), () => 0),
@@ -268,6 +268,11 @@ class ExpressionService {
268
268
  }
269
269
  return { [item]: sub };
270
270
  }, 0);
271
+ if (_.isArray(nested) || _.isArray(accum[first])) {
272
+ accum[first] = nested;
273
+ } else {
274
+ accum[first] = _.merge(accum[first] || {}, nested);
275
+ }
271
276
  } else {
272
277
  accum[first] = 0;
273
278
  }
@@ -0,0 +1,274 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const TransformState = require('./transformState');
5
+
6
+ class BranchChain {
7
+ constructor(chain) {
8
+ this.chain = chain;
9
+ this.opResult = null;
10
+
11
+ const end = this.chain.chainEnd();
12
+ end.on('update', (value) => {
13
+ this.opResult = value;
14
+ });
15
+ end.on('unavailable', () => {
16
+ this.opResult = null;
17
+ });
18
+ }
19
+
20
+ update(context, value, time) {
21
+ this.chain.update(context, value, time);
22
+ return this.opResult;
23
+ }
24
+
25
+ probe(context, time) {
26
+ this.chain.probe(context, time);
27
+ }
28
+
29
+ probeOrForward(context, time) {
30
+ this.chain.probeOrForward(context, time);
31
+ }
32
+
33
+ setUnavailable(context, time) {
34
+ this.chain.setUnavailable(context, time);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Conditional branching transform. Evaluates conditions in sequence and runs the first
40
+ * matching branch's transform chain. Analogous to if/else-if/else.
41
+ *
42
+ * Config format (after preprocessing):
43
+ * - branch:
44
+ * - if:
45
+ * - condition: this == 1 or x > y
46
+ * - op1
47
+ * - op2
48
+ * - else-if:
49
+ * - condition: this == 2
50
+ * - op1
51
+ * - else:
52
+ * - op1
53
+ */
54
+ class BranchFilter extends TransformState {
55
+ constructor({ engine, args: { conditions, branchChains, elseChain } }, builder) {
56
+ super();
57
+
58
+ this.engine = engine;
59
+ this.conditions = conditions;
60
+ this.branchChains = _.map(branchChains, (transforms) => {
61
+ const chain = builder.createTransformChain(engine, transforms);
62
+ return new BranchChain(chain);
63
+ });
64
+ this.elseChain = elseChain
65
+ ? new BranchChain(builder.createTransformChain(engine, elseChain))
66
+ : null;
67
+ }
68
+
69
+ static op = 'branch';
70
+
71
+ static create(args) {
72
+ return new BranchFilter(args, args.builder);
73
+ }
74
+
75
+ static parseConfig(configUtil, defn) {
76
+ const components = defn.args;
77
+ if (!_.isArray(components)) {
78
+ configUtil.throwConfigError('branch must contain a list of if/else-if/else components', this.op);
79
+ }
80
+
81
+ const conditions = [];
82
+ const branchChains = [];
83
+ let elseChain = null;
84
+ let lastWasConditional = false;
85
+
86
+ components.forEach((comp, i) => {
87
+ if (!_.isObject(comp) || _.isArray(comp)) {
88
+ configUtil.throwConfigError('Invalid branch component', this.op);
89
+ }
90
+
91
+ const key = _.first(_.keys(comp));
92
+ const body = comp[key];
93
+
94
+ if (!_.isArray(body)) {
95
+ configUtil.throwConfigError(`${key} must contain a list of transforms`, this.op);
96
+ }
97
+
98
+ if (key === 'if' || key === 'else-if') {
99
+ if (body.length === 0) {
100
+ configUtil.throwConfigError(`${key} must have at least a condition`, this.op);
101
+ }
102
+
103
+ const first = body[0];
104
+ let conditionExpr = null;
105
+ if (_.isObject(first) && !_.isArray(first) && _.has(first, 'condition')) {
106
+ conditionExpr = first.condition;
107
+ if (_.isObject(conditionExpr) && _.has(conditionExpr, 'expression')) {
108
+ conditionExpr = conditionExpr.expression;
109
+ }
110
+ conditionExpr = conditionExpr.toString();
111
+ }
112
+
113
+ if (!conditionExpr) {
114
+ configUtil.throwConfigError(`${key} first element must be condition: <expression>`, this.op);
115
+ }
116
+
117
+ const compiledExpression = configUtil.compileExpression(conditionExpr, this.op, { this: 0 });
118
+ conditions.push({
119
+ expression: conditionExpr,
120
+ compiledExpression,
121
+ expressionSources: configUtil.expressionService.expressionTriggers(`${defn.path}.${i}.condition`),
122
+ hasThis: configUtil.expressionService.findName(conditionExpr, 'this'),
123
+ sourceCache: {},
124
+ });
125
+
126
+ const chainTransforms = body.slice(1);
127
+ const { transform } = configUtil.loadTransformList(chainTransforms, `${defn.path}.${i}.${key}`, false);
128
+ branchChains.push(transform);
129
+ lastWasConditional = true;
130
+ } else if (key === 'else') {
131
+ if (!lastWasConditional) {
132
+ configUtil.throwConfigError('else must follow if or else-if', this.op);
133
+ }
134
+ if (elseChain !== null) {
135
+ configUtil.throwConfigError('only one else allowed per branch', this.op);
136
+ }
137
+
138
+ const { transform } = configUtil.loadTransformList(body, `${defn.path}.${i}.else`, false);
139
+ elseChain = transform;
140
+ lastWasConditional = false;
141
+ } else {
142
+ configUtil.throwConfigError(`Unknown branch component: ${key}`, this.op);
143
+ }
144
+ });
145
+
146
+ defn.args = {
147
+ conditions,
148
+ branchChains,
149
+ elseChain,
150
+ };
151
+ }
152
+
153
+ static getExpressions(configUtil, body, info) {
154
+ const { varName, index, attribute } = info;
155
+ const parts = [];
156
+
157
+ if (!_.isArray(body)) {
158
+ return [];
159
+ }
160
+
161
+ _.each(body, (comp, compIndex) => {
162
+ if (!_.isObject(comp) || _.isArray(comp)) {
163
+ return;
164
+ }
165
+
166
+ const key = _.first(_.keys(comp));
167
+ const compBody = comp[key];
168
+
169
+ if (key === 'if' || key === 'else-if') {
170
+ if (_.isArray(compBody) && compBody.length > 0) {
171
+ const first = compBody[0];
172
+ if (_.isObject(first) && _.has(first, 'condition')) {
173
+ const expr = first.condition?.toString?.() ?? first.condition;
174
+ if (expr) {
175
+ parts.push({
176
+ path: `variables.${varName}.${index}.${attribute}.${compIndex}.condition`,
177
+ expression: expr,
178
+ }, {
179
+ path: `variables.${varName}`,
180
+ expression: expr,
181
+ });
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ if (key === 'if' || key === 'else-if' || key === 'else') {
188
+ const chainTransforms = key === 'else'
189
+ ? compBody
190
+ : compBody.slice(1);
191
+ const normalized = _.map(chainTransforms, (t) => (_.isString(t) ? { [t]: [] } : t));
192
+ // Include key (if/else-if/else) so expression paths match loadTransformList (used by parseConfig)
193
+ const chainVarName = `${varName}.${index}.${attribute}.${compIndex}.${key}`;
194
+ const chainExprs = configUtil.getExpressionsFromChain(
195
+ normalized,
196
+ chainVarName,
197
+ [...(info.ancestorVars || []), varName]
198
+ );
199
+ parts.push(..._.flatten(chainExprs));
200
+ }
201
+ });
202
+
203
+ return parts;
204
+ }
205
+
206
+ supportsPendingChanges() {
207
+ const chains = [...this.branchChains, this.elseChain].filter(Boolean);
208
+ return _.some(chains, (c) => c.chain.chainSupportsPendingChange?.());
209
+ }
210
+
211
+ selectBranch(context, value, time) {
212
+ for (let i = 0; i < this.conditions.length; i += 1) {
213
+ const rule = this.conditions[i];
214
+
215
+ if (rule.hasThis) {
216
+ rule.sourceCache.this = value;
217
+ }
218
+
219
+ const unavailTerm = _.reduce(rule.expressionSources, (res, name) => {
220
+ const state = this.engine.getState(name);
221
+ if (state) {
222
+ rule.sourceCache[name] = state.value;
223
+ }
224
+ return res || !state || !state.available;
225
+ }, false);
226
+
227
+ if (unavailTerm) {
228
+ return -1;
229
+ }
230
+
231
+ try {
232
+ if (rule.compiledExpression.evaluate(rule.sourceCache)) {
233
+ return i;
234
+ }
235
+ } catch (err) {
236
+ this.recordError(context, err, time);
237
+ }
238
+ }
239
+
240
+ return this.elseChain !== null ? this.conditions.length : -1;
241
+ }
242
+
243
+ update(context, value, time) {
244
+ if (value === 'UNAVAILABLE') {
245
+ this.setUnavailable(context, time);
246
+ } else {
247
+ super.update(context, value, time);
248
+ }
249
+ }
250
+
251
+ filter(context, value, time) {
252
+ const branchIndex = this.selectBranch(context, value, time);
253
+
254
+ if (branchIndex < 0) {
255
+ this.commitValue(context, value, time);
256
+ return;
257
+ }
258
+
259
+ const chain = branchIndex < this.branchChains.length ? this.branchChains[branchIndex] : this.elseChain;
260
+
261
+ const result = chain.update(context, value, time);
262
+ if (result !== null && result !== undefined) {
263
+ this.commitValue(context, result, time);
264
+ } else {
265
+ this.setUnavailable(context, time);
266
+ }
267
+ }
268
+
269
+ setUnavailable(context, time) {
270
+ super.setUnavailable(context, time);
271
+ }
272
+ }
273
+
274
+ module.exports = BranchFilter;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const AccumulateFilter = require('./accumulate');
4
4
  const AverageFilter = require('./average');
5
+ const BranchFilter = require('./branch');
5
6
  const DebounceFilter = require('./debounce');
6
7
  const DownsampleFilter = require('./downsample');
7
8
  const EdgeFilter = require('./edge');
@@ -48,6 +49,7 @@ const WindowCountFilter = require('./windowCount');
48
49
  module.exports = {
49
50
  accumulate: AccumulateFilter,
50
51
  average: AverageFilter,
52
+ branch: BranchFilter,
51
53
  debounce: DebounceFilter,
52
54
  downsample: DownsampleFilter,
53
55
  edge: EdgeFilter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machinemetrics/io-adapter-lib",
3
- "version": "2.35.1",
3
+ "version": "2.37.0",
4
4
  "description": "Configuration and engine implementation for MachineMetrics AdapterScripts and adapters",
5
5
  "main": "index.js",
6
6
  "license": "UNLICENSED",
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const ConfigError = require('../../lib/config/configError');
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('Brother FTP device config', function () {
8
+ it('accepts brother-ftp without collection-mode (defaults to ftp)', async function () {
9
+ const cfg = await testUtils.loadConfig('device/brother-ftp-minimal.yml');
10
+ expect(cfg.deviceName).to.eq('brother-ftp');
11
+ expect(cfg.device.dataCollectionMode).to.eq(cfg.device.DataCollectionModes.FTP);
12
+ });
13
+
14
+ it('passes through optional FTP paths and cnc-version', async function () {
15
+ const cfg = await testUtils.loadConfig('device/brother-ftp-paths.yml');
16
+ expect(cfg.device.cncVersion).to.eq('d00');
17
+ expect(cfg.device.memoryPath).to.eq('_DAT/MEM.NC');
18
+ expect(cfg.device.productionPath).to.eq('_PRD/PRDD2.NC');
19
+ expect(cfg.device.panelPath).to.eq('_DAT/PANEL.NC');
20
+ expect(cfg.device.positionPath).to.eq('_DAT/PDSP.NC');
21
+ expect(cfg.device.workCounterPath).to.eq('_DAT/WKCNTR.NC');
22
+ expect(cfg.device.alarmPath).to.eq('_DAT/ALARM.NC');
23
+ expect(cfg.device.macroPath).to.eq('_DAT/MCRNI1.NC');
24
+ });
25
+
26
+ it('does not set path properties when keys are omitted', async function () {
27
+ const cfg = await testUtils.loadConfig('device/brother-ftp-minimal.yml');
28
+ expect(cfg.device.memoryPath).to.eq(undefined);
29
+ expect(cfg.device.cncVersion).to.eq(undefined);
30
+ });
31
+
32
+ it('accepts cnc-version legacy', async function () {
33
+ const cfg = await testUtils.loadConfig('device/brother-ftp-cnc-legacy.yml');
34
+ expect(cfg.device.cncVersion).to.eq('legacy');
35
+ });
36
+
37
+ it('rejects invalid cnc-version', async function () {
38
+ try {
39
+ await testUtils.loadConfig('device/brother-ftp-bad-cnc.yml');
40
+ expect.fail('Expected ConfigError');
41
+ } catch (err) {
42
+ expect(err).to.be.instanceof(ConfigError);
43
+ }
44
+ });
45
+
46
+ it('rejects paths containing ..', async function () {
47
+ try {
48
+ await testUtils.loadConfig('device/brother-ftp-bad-path.yml');
49
+ expect.fail('Expected ConfigError');
50
+ } catch (err) {
51
+ expect(err).to.be.instanceof(ConfigError);
52
+ }
53
+ });
54
+
55
+ it('rejects brother-ftp with non-ftp collection-mode', async function () {
56
+ try {
57
+ await testUtils.loadConfig('device/brother-ftp-bad-mode.yml');
58
+ expect.fail('Expected ConfigError');
59
+ } catch (err) {
60
+ expect(err).to.be.instanceof(ConfigError);
61
+ }
62
+ });
63
+ });
@@ -15,5 +15,7 @@ describe('Brother HTTP config tests', function () {
15
15
  expect(defaultConfig.device.port).to.eq(21);
16
16
  expect(defaultConfig.device.scanInterval).to.eq(1000);
17
17
  expect(defaultConfig.device.httpInterval).to.eq(50);
18
+ expect(defaultConfig.device.memoryPath).to.eq(undefined);
19
+ expect(defaultConfig.device.cncVersion).to.eq(undefined);
18
20
  });
19
21
  });
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ cnc-version: bogus
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ collection-mode: http
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ memory-path: _DAT/../MEM.NC
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ cnc-version: legacy
@@ -0,0 +1,2 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
@@ -0,0 +1,11 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ collection-mode: ftp
4
+ cnc-version: d00
5
+ memory-path: _DAT/MEM.NC
6
+ production-path: _PRD/PRDD2.NC
7
+ panel-path: _DAT/PANEL.NC
8
+ position-path: _DAT/PDSP.NC
9
+ work-counter-path: _DAT/WKCNTR.NC
10
+ alarm-path: _DAT/ALARM.NC
11
+ macro-path: _DAT/MCRNI1.NC
@@ -0,0 +1,117 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ - x
8
+ - y
9
+ - z
10
+ - obj
11
+ variables:
12
+ branch1:
13
+ - source: counter
14
+ - if:
15
+ - condition: this == 1
16
+ - expression: 100
17
+ - else-if:
18
+ - condition: this == 2
19
+ - expression: 200
20
+ - else:
21
+ - expression: 0
22
+ branch2simple:
23
+ - source: counter
24
+ - if:
25
+ - condition: this == 1
26
+ - expression: 10
27
+ - else:
28
+ - expression: 0
29
+ branch3:
30
+ - source: counter
31
+ - if:
32
+ - condition: x > y
33
+ - expression: 1
34
+ - else-if:
35
+ - condition: x < y
36
+ - expression: -1
37
+ - else:
38
+ - expression: 0
39
+ ifOnly:
40
+ - source: counter
41
+ - if:
42
+ - condition: this == 1
43
+ - expression: 100
44
+ ifElseIf:
45
+ - source: counter
46
+ - if:
47
+ - condition: this == 1
48
+ - expression: 100
49
+ - else-if:
50
+ - condition: this == 2
51
+ - expression: 200
52
+ twoElseIfs:
53
+ - source: counter
54
+ - if:
55
+ - condition: this == 1
56
+ - expression: 10
57
+ - else-if:
58
+ - condition: this == 2
59
+ - expression: 20
60
+ - else-if:
61
+ - condition: this == 3
62
+ - expression: 30
63
+ - else:
64
+ - expression: 0
65
+ ifElseIfConsecutive:
66
+ - source: counter
67
+ - if:
68
+ - condition: this == 1
69
+ - expression: 100
70
+ - else:
71
+ - expression: 2
72
+ - if:
73
+ - condition: counter == 2
74
+ - expression: 200
75
+ branchWithThis:
76
+ - source: counter
77
+ - if:
78
+ - condition: this == 1
79
+ - expression: 100
80
+ - else:
81
+ - expression: this
82
+ branchUnavailable:
83
+ - source: counter
84
+ - if:
85
+ - condition: this == 1
86
+ - expression: x + 1
87
+ - else:
88
+ - expression: this
89
+ branchWithInnerVar:
90
+ - source: counter
91
+ - if:
92
+ - condition: obj.a > 5
93
+ - expression: obj.a
94
+ - else:
95
+ - expression: obj.b
96
+ nested:
97
+ - source: counter
98
+ - if:
99
+ - condition: this > 0
100
+ - if:
101
+ - condition: this > 2
102
+ - expression: z
103
+ - else:
104
+ - expression: y
105
+ - else:
106
+ - expression: x
107
+ nestedInnerVar:
108
+ - source: counter
109
+ - if:
110
+ - condition: this > 0
111
+ - if:
112
+ - condition: obj.a > obj.b
113
+ - expression: obj.a + obj.b
114
+ - else:
115
+ - expression: y
116
+ - else:
117
+ - expression: x
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - else:
11
+ - expression: 0
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - condition: this == 1
12
+ - expression: 100
13
+ - else-if:
14
+ - expression: 200
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - else-if:
11
+ - condition: this == 1
12
+ - expression: 100
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - expression: 100
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - condition: this == 1
12
+ - expression: 100
13
+ - invert
14
+ - else-if:
15
+ - condition: this == 2
16
+ - expression: 200
17
+ - else:
18
+ - expression: 0
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const ConfigError = require('../../lib/config/configError');
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('branch transform invalid config tests', function () {
8
+ it('rejects else by itself', async function () {
9
+ try {
10
+ await testUtils.loadConfig('transform/invalid-branch-else-only.yml');
11
+ expect.fail('Config with else by itself should have thrown');
12
+ } catch (err) {
13
+ expect(err).to.be.instanceof(ConfigError);
14
+ expect(err.message).to.include('else');
15
+ }
16
+ });
17
+
18
+ it('rejects else-if by itself', async function () {
19
+ try {
20
+ await testUtils.loadConfig('transform/invalid-branch-elseif-only.yml');
21
+ expect.fail('Config with else-if by itself should have thrown');
22
+ } catch (err) {
23
+ expect(err).to.be.instanceof(ConfigError);
24
+ expect(err.message).to.include('else-if');
25
+ }
26
+ });
27
+
28
+ it('rejects if without condition as first transform', async function () {
29
+ try {
30
+ await testUtils.loadConfig('transform/invalid-branch-if-no-condition.yml');
31
+ expect.fail('Config with if without condition should have thrown');
32
+ } catch (err) {
33
+ expect(err).to.be.instanceof(ConfigError);
34
+ expect(err.message).to.include('condition');
35
+ }
36
+ });
37
+
38
+ it('rejects else-if without condition as first transform', async function () {
39
+ try {
40
+ await testUtils.loadConfig('transform/invalid-branch-elseif-no-condition.yml');
41
+ expect.fail('Config with else-if without condition should have thrown');
42
+ } catch (err) {
43
+ expect(err).to.be.instanceof(ConfigError);
44
+ expect(err.message).to.include('condition');
45
+ }
46
+ });
47
+
48
+ it('rejects unrelated op between if/else-if/else', async function () {
49
+ try {
50
+ await testUtils.loadConfig('transform/invalid-branch-unrelated-op.yml');
51
+ expect.fail('Config with unrelated op between if/else-if/else should have thrown');
52
+ } catch (err) {
53
+ expect(err).to.be.instanceof(ConfigError);
54
+ expect(err.message).to.match(/else-if|else|Unsupported/);
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const EngineV2 = require('../../lib/engine/engineV2');
5
+ const Builder = require('../../lib/engine/transformBuilderV2');
6
+ const testUtils = require('../util/testUtils');
7
+
8
+ describe('branch transform tests', function () {
9
+ let config;
10
+ before(async () => {
11
+ config = await testUtils.loadConfig('transform/branch.yml');
12
+ });
13
+
14
+ it('selects if branch when condition matches', function () {
15
+ const engine = new EngineV2(config);
16
+ const builder = new Builder(config);
17
+ builder.build(engine);
18
+
19
+ const source = testUtils.valueSource();
20
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
21
+
22
+ source.sendValue('counter', 1, 0);
23
+
24
+ engine.validateFilter([
25
+ [100, 0],
26
+ ]);
27
+ });
28
+
29
+ it('selects else-if branch when condition matches', function () {
30
+ const engine = new EngineV2(config);
31
+ const builder = new Builder(config);
32
+ builder.build(engine);
33
+
34
+ const source = testUtils.valueSource();
35
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
36
+
37
+ source.sendValue('counter', 2, 0);
38
+
39
+ engine.validateFilter([
40
+ [200, 0],
41
+ ]);
42
+ });
43
+
44
+ it('selects else branch when no condition matches', function () {
45
+ const engine = new EngineV2(config);
46
+ const builder = new Builder(config);
47
+ builder.build(engine);
48
+
49
+ const source = testUtils.valueSource();
50
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
51
+
52
+ source.sendValue('counter', 5, 0);
53
+
54
+ engine.validateFilter([
55
+ [0, 0],
56
+ ]);
57
+ });
58
+
59
+ it('evaluates constant expression in branch', function () {
60
+ const engine = new EngineV2(config);
61
+ const builder = new Builder(config);
62
+ builder.build(engine);
63
+
64
+ const source = testUtils.valueSource();
65
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch2simple, source);
66
+
67
+ source.sendValue('counter', 1, 0);
68
+
69
+ engine.validateFilter([
70
+ [10, 0],
71
+ ]);
72
+ });
73
+
74
+ it('uses external variables in conditions', function () {
75
+ const engine = new EngineV2(config);
76
+ const builder = new Builder(config);
77
+ builder.build(engine);
78
+
79
+ const source = testUtils.valueSource();
80
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
81
+
82
+ source.sendValue('x', 10, 0);
83
+ source.sendValue('y', 5, 0);
84
+ source.sendValue('counter', 0, 0);
85
+
86
+ engine.validateFilter([
87
+ [1, 0],
88
+ ]);
89
+ });
90
+
91
+ it('selects else-if when x < y', function () {
92
+ const engine = new EngineV2(config);
93
+ const builder = new Builder(config);
94
+ builder.build(engine);
95
+
96
+ const source = testUtils.valueSource();
97
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
98
+
99
+ source.sendValue('x', 5, 0);
100
+ source.sendValue('y', 10, 0);
101
+ source.sendValue('counter', 0, 0);
102
+
103
+ engine.validateFilter([
104
+ [-1, 0],
105
+ ]);
106
+ });
107
+
108
+ it('selects else when x == y', function () {
109
+ const engine = new EngineV2(config);
110
+ const builder = new Builder(config);
111
+ builder.build(engine);
112
+
113
+ const source = testUtils.valueSource();
114
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
115
+
116
+ source.sendValue('x', 5, 0);
117
+ source.sendValue('y', 5, 0);
118
+ source.sendValue('counter', 0, 0);
119
+
120
+ engine.validateFilter([
121
+ [0, 0],
122
+ ]);
123
+ });
124
+
125
+ it('if only: selects branch when condition matches', function () {
126
+ const engine = new EngineV2(config);
127
+ const builder = new Builder(config);
128
+ builder.build(engine);
129
+
130
+ const source = testUtils.valueSource();
131
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifOnly, source);
132
+
133
+ source.sendValue('counter', 1, 0);
134
+
135
+ engine.validateFilter([
136
+ [100, 0],
137
+ ]);
138
+ });
139
+
140
+ it('if only: passes through when condition does not match', function () {
141
+ const engine = new EngineV2(config);
142
+ const builder = new Builder(config);
143
+ builder.build(engine);
144
+
145
+ const source = testUtils.valueSource();
146
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifOnly, source);
147
+
148
+ source.sendValue('counter', 5, 0);
149
+
150
+ engine.validateFilter([
151
+ [5, 0],
152
+ ]);
153
+ });
154
+
155
+ it('if/else-if without else: selects first branch', function () {
156
+ const engine = new EngineV2(config);
157
+ const builder = new Builder(config);
158
+ builder.build(engine);
159
+
160
+ const source = testUtils.valueSource();
161
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
162
+
163
+ source.sendValue('counter', 1, 0);
164
+
165
+ engine.validateFilter([
166
+ [100, 0],
167
+ ]);
168
+ });
169
+
170
+ it('if/else-if without else: selects second branch', function () {
171
+ const engine = new EngineV2(config);
172
+ const builder = new Builder(config);
173
+ builder.build(engine);
174
+
175
+ const source = testUtils.valueSource();
176
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
177
+
178
+ source.sendValue('counter', 2, 0);
179
+
180
+ engine.validateFilter([
181
+ [200, 0],
182
+ ]);
183
+ });
184
+
185
+ it('if/else-if without else: passes through when neither matches', function () {
186
+ const engine = new EngineV2(config);
187
+ const builder = new Builder(config);
188
+ builder.build(engine);
189
+
190
+ const source = testUtils.valueSource();
191
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
192
+
193
+ source.sendValue('counter', 5, 0);
194
+
195
+ engine.validateFilter([
196
+ [5, 0],
197
+ ]);
198
+ });
199
+
200
+ it('two else-ifs: selects each branch correctly', function () {
201
+ const engine = new EngineV2(config);
202
+ const builder = new Builder(config);
203
+ builder.build(engine);
204
+
205
+ const source = testUtils.valueSource();
206
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.twoElseIfs, source);
207
+
208
+ source.sendValue('counter', 1, 0);
209
+ source.sendValue('counter', 2, 1);
210
+ source.sendValue('counter', 3, 2);
211
+ source.sendValue('counter', 5, 3);
212
+
213
+ engine.validateFilter([[10, 0], [20, 1], [30, 2], [0, 3]]);
214
+ });
215
+
216
+ it('if/else/if: two consecutive branches', function () {
217
+ const engine = new EngineV2(config);
218
+ const builder = new Builder(config);
219
+ builder.build(engine);
220
+
221
+ const source = testUtils.valueSource();
222
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIfConsecutive, source);
223
+
224
+ source.sendValue('counter', 1, 0);
225
+ source.sendValue('counter', 2, 1);
226
+ source.sendValue('counter', 3, 2);
227
+
228
+ engine.validateFilter([[100, 0], [200, 1], [2, 2]]);
229
+ });
230
+
231
+ it('expression: this in else branch passes through input value', function () {
232
+ const engine = new EngineV2(config);
233
+ const builder = new Builder(config);
234
+ builder.build(engine);
235
+
236
+ const source = testUtils.valueSource();
237
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branchWithThis, source);
238
+
239
+ source.sendValue('counter', 1, 0);
240
+ source.sendValue('counter', 5, 1);
241
+
242
+ engine.validateFilter([[100, 0], [5, 1]]);
243
+ });
244
+
245
+ it('unavailable in taken branch propagates; unavailable in not-taken branch has no effect', function () {
246
+ const engine = new EngineV2(config);
247
+ const builder = new Builder(config);
248
+ builder.build(engine);
249
+
250
+ const source = testUtils.valueSource();
251
+ engine.addValueSource(source);
252
+
253
+ const filterUpdates = [];
254
+ const endFilter = engine.variablePool.branchUnavailable.chainEnd();
255
+ endFilter.on('update', (value, time) => {
256
+ filterUpdates.push({ time, value });
257
+ });
258
+ endFilter.on('unavailable', (time) => {
259
+ filterUpdates.push({ time, value: 'UNAVAILABLE' });
260
+ });
261
+
262
+ const expectFilter = (expected) => {
263
+ const ref = expected.map(([value, time]) => ({ time, value }));
264
+ expect(filterUpdates.length).to.eq(ref.length);
265
+ ref.forEach((r, i) => {
266
+ expect(filterUpdates[i].time).to.be.closeTo(r.time, 0.000001);
267
+ expect(filterUpdates[i].value).to.deep.eq(r.value);
268
+ });
269
+ };
270
+
271
+ source.sendValue('x', 10, 0);
272
+ source.sendValue('counter', 1, 0);
273
+ source.sendValue('x', 'UNAVAILABLE', 1);
274
+ source.sendValue('counter', 5, 2);
275
+ source.sendValue('x', 'UNAVAILABLE', 2);
276
+
277
+ expectFilter([['UNAVAILABLE', 0], [11, 0], ['UNAVAILABLE', 1], ['UNAVAILABLE', 1], [5, 2]]);
278
+ });
279
+
280
+ it('nested if/else inside branch', function () {
281
+ const engine = new EngineV2(config);
282
+ const builder = new Builder(config);
283
+ builder.build(engine);
284
+
285
+ const source = testUtils.valueSource();
286
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nested, source);
287
+
288
+ source.sendValue('x', 0, 0);
289
+ source.sendValue('y', 150, 0);
290
+ source.sendValue('z', 200, 0);
291
+
292
+ source.sendValue('counter', 0, 0);
293
+ source.sendValue('counter', 1, 1);
294
+ source.sendValue('counter', 2, 2);
295
+ source.sendValue('counter', 3, 3);
296
+
297
+ engine.validateFilter([[0, 0], [150, 1], [150, 2], [200, 3]]);
298
+ });
299
+
300
+ it('nested branch re-evaluates when expression variable in taken path updates', function () {
301
+ const engine = new EngineV2(config);
302
+ const builder = new Builder(config);
303
+ builder.build(engine);
304
+
305
+ const source = testUtils.valueSource();
306
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nested, source);
307
+
308
+ source.sendValue('x', 0, 0);
309
+ source.sendValue('y', 150, 0);
310
+ source.sendValue('z', 200, 0);
311
+
312
+ source.sendValue('counter', 0, 0);
313
+ source.sendValue('x', 1, 1);
314
+ source.sendValue('counter', 1, 2);
315
+ source.sendValue('y', 151, 3);
316
+ source.sendValue('counter', 3, 4);
317
+ source.sendValue('z', 201, 5);
318
+
319
+ engine.validateFilter([[0, 0], [1, 1], [150, 2], [151, 3], [200, 4], [201, 5]]);
320
+ });
321
+
322
+ it('branchWithInnerVar selects branch by obj.a and emits obj.a or obj.b', function () {
323
+ const engine = new EngineV2(config);
324
+ const builder = new Builder(config);
325
+ builder.build(engine);
326
+
327
+ const source = testUtils.valueSource();
328
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branchWithInnerVar, source);
329
+
330
+ source.sendValue('obj', { a: 10, b: 1 }, 0);
331
+ source.sendValue('counter', 1, 0);
332
+ source.sendValue('obj', { a: 3, b: 2 }, 1);
333
+ source.sendValue('counter', 2, 1);
334
+
335
+ engine.validateFilter([[10, 0], [2, 1]]);
336
+ });
337
+
338
+ it('nestedInnerVar selects nested branches by this and obj.a, obj.b', function () {
339
+ const engine = new EngineV2(config);
340
+ const builder = new Builder(config);
341
+ builder.build(engine);
342
+
343
+ const source = testUtils.valueSource();
344
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nestedInnerVar, source);
345
+
346
+ source.sendValue('x', 0, 0);
347
+ source.sendValue('y', 150, 0);
348
+ source.sendValue('obj', { a: 10, b: 5 }, 0);
349
+
350
+ source.sendValue('counter', 0, 0);
351
+ source.sendValue('counter', 1, 1);
352
+ source.sendValue('obj', { a: 3, b: 8 }, 2);
353
+ source.sendValue('counter', 2, 2);
354
+
355
+ engine.validateFilter([[0, 0], [15, 1], [150, 2]]);
356
+ });
357
+ });