@machinemetrics/io-adapter-lib 2.33.0 → 2.35.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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.35.0]
4
+ - Added data-events config support
5
+
6
+ ## [2.34.0]
7
+ - Added support for initializing variables from constant source or expression ops
8
+ - Fixed map not recognizing external identifiers in sub-ops
9
+ - Allow string and bool constants in shorthand variable declarations
10
+
3
11
  ## [2.33.0]
4
12
  - Added support for numeric indexing in expressions
5
13
  - Fixed map not allowing any transforms to be chained beyond it
@@ -100,7 +100,20 @@ class AdapterConfig {
100
100
  });
101
101
  }).flattenDeep().compact().value();
102
102
 
103
- return [...dataItems, ...conditions];
103
+ const dataEvents = _(config['data-events']).map((event, key) => {
104
+ const exprs = [];
105
+ if (_.isString(event.payload)) {
106
+ exprs.push({ path: `data-events.${key}.payload`, expression: event.payload });
107
+ }
108
+ _.each(event.triggers, (trigger, i) => {
109
+ if (_.isString(trigger)) {
110
+ exprs.push({ path: `data-events.${key}.triggers.${i}`, expression: trigger });
111
+ }
112
+ });
113
+ return exprs;
114
+ }).flattenDeep().compact().value();
115
+
116
+ return [...dataItems, ...conditions, ...dataEvents];
104
117
  }
105
118
 
106
119
  getStringExpressions(config = {}) {
@@ -150,6 +163,7 @@ class AdapterConfig {
150
163
  parse(config = {}) {
151
164
  this.loadDataItems(config['data-items']);
152
165
  this.loadConditions(config.conditions);
166
+ this.loadDataEvents(config['data-events']);
153
167
 
154
168
  this.passthroughConditions = _(this.conditions).filter((cond) => {
155
169
  return cond.passthroughSource;
@@ -524,6 +538,62 @@ class AdapterConfig {
524
538
  };
525
539
  }
526
540
 
541
+ loadDataEvents(events) {
542
+ this.dataEventsByExpression = {};
543
+ this.dataEvents = _.mapValues(events || {}, (event, key) => {
544
+ if (!_.isString(event.payload)) {
545
+ const configErr = new ConfigError(`Data event '${key}' has no payload`);
546
+ throw configErr.atSection(`data-events.${key}`).atAttribute('payload');
547
+ }
548
+
549
+ let payloadDefn;
550
+ try {
551
+ const payloadPath = `data-events.${key}.payload`;
552
+ const compiledPayload = this.expressionService.compileExpression(event.payload);
553
+ this.dataEventsByExpression[payloadPath] = key;
554
+ payloadDefn = {
555
+ rawExpression: event.payload,
556
+ expression: compiledPayload,
557
+ };
558
+ } catch (err) {
559
+ const configErr = new ConfigError(`Problem evaluating payload expression: ${err.message}`);
560
+ throw configErr.atSection(`data-events.${key}`).atAttribute('payload');
561
+ }
562
+
563
+ if (!_.isArray(event.triggers) || _.isEmpty(event.triggers)) {
564
+ const configErr = new ConfigError(`Data event '${key}' has no triggers`);
565
+ throw configErr.atSection(`data-events.${key}`).atAttribute('triggers');
566
+ }
567
+
568
+ const triggers = _.map(event.triggers, (trigger, i) => {
569
+ if (!_.isString(trigger)) {
570
+ const configErr = new ConfigError(`Data event '${key}' trigger at index ${i} must be a string expression`);
571
+ throw configErr.atSection(`data-events.${key}.triggers`);
572
+ }
573
+
574
+ try {
575
+ const triggerPath = `data-events.${key}.triggers.${i}`;
576
+ const compiledTrigger = this.expressionService.compileExpression(trigger);
577
+ this.dataEventsByExpression[triggerPath] = key;
578
+ return {
579
+ rawExpression: trigger,
580
+ expression: compiledTrigger,
581
+ triggerVariables: this.expressionService.expressionTriggers(triggerPath),
582
+ };
583
+ } catch (err) {
584
+ const configErr = new ConfigError(`Problem evaluating trigger expression: ${err.message}`);
585
+ throw configErr.atSection(`data-events.${key}.triggers`).atAttribute(i);
586
+ }
587
+ });
588
+
589
+ return {
590
+ name: key,
591
+ payload: payloadDefn,
592
+ triggers,
593
+ };
594
+ });
595
+ }
596
+
527
597
  triggeredDataItems(name) {
528
598
  const triggeredPaths = this.expressionService.expressionsTriggeredBy(name);
529
599
  return _(triggeredPaths).map(p => this.dataItemsByExpression[p]).compact().uniq().value();
@@ -533,6 +603,11 @@ class AdapterConfig {
533
603
  const triggeredPaths = this.expressionService.expressionsTriggeredBy(name);
534
604
  return _(triggeredPaths).map(p => this.conditionsByExpression[p]).compact().uniq().value();
535
605
  }
606
+
607
+ triggeredDataEvents(name) {
608
+ const triggeredPaths = this.expressionService.expressionsTriggeredBy(name);
609
+ return _(triggeredPaths).map(p => this.dataEventsByExpression[p]).compact().uniq().value();
610
+ }
536
611
  }
537
612
 
538
613
  module.exports = AdapterConfig;
@@ -42,7 +42,7 @@ class EngineConfig {
42
42
  // - source: composite.parts
43
43
 
44
44
  this.config.variables = _.mapValues(this.config.variables, (variable) => {
45
- if (_.isString(variable)) {
45
+ if (_.isString(variable) || _.isNumber(variable) || _.isBoolean(variable)) {
46
46
  return [{ source: variable }];
47
47
  }
48
48
  return variable;
@@ -94,18 +94,22 @@ class TransformConfigUtil {
94
94
 
95
95
  getExpressionsFromVariables(variables) {
96
96
  return _(variables).map((defn, varName) => {
97
- return _.map(defn, (transformContainer, index) => {
98
- return _.map(transformContainer, (body, attribute) => {
99
- if (this.transformMap[attribute]) {
100
- return this.transformMap[attribute].getExpressions(this, body, { varName, index, attribute });
101
- }
102
- return [];
103
- });
104
- });
97
+ return this.getExpressionsFromChain(defn, varName);
105
98
  }).flattenDeep().compact().value();
106
99
  }
107
100
 
108
- getExpressions(body, { varName, index, attribute, field }, includeDefault = false) {
101
+ getExpressionsFromChain(chain, varName, ancestorVars = []) {
102
+ return _.map(chain, (transformContainer, index) => {
103
+ return _.map(transformContainer, (body, attribute) => {
104
+ if (this.transformMap[attribute]) {
105
+ return this.transformMap[attribute].getExpressions(this, body, { varName, index, attribute, ancestorVars });
106
+ }
107
+ return [];
108
+ });
109
+ });
110
+ }
111
+
112
+ getExpressions(body, { varName, index, attribute, field, ancestorVars }, includeDefault = false) {
109
113
  const parts = [];
110
114
 
111
115
  let expression = '';
@@ -140,6 +144,13 @@ class TransformConfigUtil {
140
144
  expression,
141
145
  });
142
146
 
147
+ _.each(ancestorVars, (ancestorVar) => {
148
+ parts.push({
149
+ path: `variables.${ancestorVar}`,
150
+ expression,
151
+ });
152
+ });
153
+
143
154
  return parts;
144
155
  }
145
156
 
@@ -34,6 +34,11 @@ class TransformBuilderV2 {
34
34
  engine.addVariable(varName, transform);
35
35
  }
36
36
  });
37
+
38
+ _.each(engine.variablePool, (chain, varName) => {
39
+ const context = { sourceType: 'init', trigger: varName };
40
+ chain.init(context, 0);
41
+ });
37
42
  }
38
43
 
39
44
  createTransformChain(engine, transformList, varName, base = false) {
@@ -9,6 +9,7 @@ class ExpressionService {
9
9
  this.availableNamesByChannel = {};
10
10
  this.referencedNamesByChannel = {};
11
11
  this.referencedNamesByPath = {};
12
+ this.selfReferencedPaths = {};
12
13
  this.pathsByReferenceName = {};
13
14
  this.defaultValues = {};
14
15
  this.channelsLookup = {};
@@ -117,6 +118,10 @@ class ExpressionService {
117
118
  }
118
119
  }
119
120
  });
121
+
122
+ if (this.findName(expression, 'this')) {
123
+ this.selfReferencedPaths[path] = true;
124
+ }
120
125
  }
121
126
 
122
127
  addStringExpression({ string, path, withThis } = {}) {
@@ -298,6 +303,10 @@ class ExpressionService {
298
303
  return this.referencedNamesByPath[path] || [];
299
304
  }
300
305
 
306
+ expressionHasSelfReference(path) {
307
+ return !!this.selfReferencedPaths[path];
308
+ }
309
+
301
310
  expressionsTriggeredBy(name) {
302
311
  return this.pathsByReferenceName[name] || [];
303
312
  }
@@ -30,6 +30,18 @@ class ExpressionFilter extends TransformState {
30
30
  this.engine = engine;
31
31
  this.compiledExpression = compiledExpression;
32
32
  this.selfSources = engine.expressionService.expressionTriggers(path);
33
+ this.hasSelfReference = engine.expressionService.expressionHasSelfReference(path);
34
+
35
+ // If there's no variables in the expression, it's a constant that can be evaluated once
36
+ if (_.isEmpty(this.selfSources) && !this.hasSelfReference) {
37
+ try {
38
+ this.constantSource = true;
39
+ this.constantValue = this.compiledExpression.evaluate();
40
+ this.lastSuppliedValue = this.constantValue;
41
+ } catch (err) {
42
+ // Oh well
43
+ }
44
+ }
33
45
  }
34
46
 
35
47
  static op = 'expression';
@@ -49,6 +61,12 @@ class ExpressionFilter extends TransformState {
49
61
  return configUtil.getExpressions(body, info);
50
62
  }
51
63
 
64
+ init(context, time) {
65
+ if (this.constantSource) {
66
+ this.update(context, this.lastSuppliedValue, time);
67
+ }
68
+ }
69
+
52
70
  update(context, value, time) {
53
71
  if (value === 'UNAVAILABLE') {
54
72
  this.setUnavailable(context, time);
@@ -58,7 +76,11 @@ class ExpressionFilter extends TransformState {
58
76
  }
59
77
 
60
78
  filter(context, value, time) {
61
- if (this.constantSource || this.simpleSource) {
79
+ if (this.constantSource) {
80
+ this.commitValue(context, this.constantValue, time);
81
+ return;
82
+ }
83
+ if (this.simpleExpression) {
62
84
  this.commitValue(context, value, time);
63
85
  return;
64
86
  }
@@ -72,6 +72,16 @@ class MapFilter extends TransformState {
72
72
  defn.args = { transforms };
73
73
  }
74
74
 
75
+ static getExpressions(configUtil, body, info) {
76
+ const varName = `${info.varName}.${info.index}.${info.attribute}`;
77
+ const ancestorVars = [
78
+ ...info.ancestorVars,
79
+ info.varName,
80
+ ];
81
+
82
+ return configUtil.getExpressionsFromChain(body, varName, ancestorVars);
83
+ }
84
+
75
85
  initReset() {
76
86
  this.chainList = [];
77
87
  this.value = [];
@@ -40,8 +40,9 @@ class SourceFilter extends TransformState {
40
40
  // If there's no variables in the expression, it's a constant that can be evaluated once
41
41
  if (_.isEmpty(this.selfSources)) {
42
42
  try {
43
- this.lastSuppliedValue = this.compiledExpression.evaluate();
44
43
  this.constantSource = true;
44
+ this.constantValue = this.compiledExpression.evaluate();
45
+ this.lastSuppliedValue = this.constantValue;
45
46
  } catch (err) {
46
47
  // Oh well
47
48
  }
@@ -77,6 +78,12 @@ class SourceFilter extends TransformState {
77
78
  return configUtil.getExpressions(body, info);
78
79
  }
79
80
 
81
+ init(context, time) {
82
+ if (this.constantSource) {
83
+ this.update(context, this.lastSuppliedValue, time);
84
+ }
85
+ }
86
+
80
87
  update(context, value, time) {
81
88
  if (value === 'UNAVAILABLE') {
82
89
  this.setUnavailable(context, time);
@@ -86,7 +93,9 @@ class SourceFilter extends TransformState {
86
93
  }
87
94
 
88
95
  filter(context, value, time) {
89
- if (this.constantSource || this.simpleSource) {
96
+ if (this.constantSource) {
97
+ this.commitValue(context, this.constantValue, time);
98
+ } else if (this.simpleSource) {
90
99
  this.commitValue(context, value, time);
91
100
  } else {
92
101
  const localState = {};
@@ -56,6 +56,9 @@ class TransformState extends EventEmitter {
56
56
  return this.pendingChangeTime;
57
57
  }
58
58
 
59
+ // Called after all varaibles have been added to the engine.
60
+ init(_context, _time) { }
61
+
59
62
  update(context, value, time) {
60
63
  if (!this.available) {
61
64
  this.available = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machinemetrics/io-adapter-lib",
3
- "version": "2.33.0",
3
+ "version": "2.35.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,75 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const expect = require('chai').expect;
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('Data events config tests', function () {
8
+ let config;
9
+ before(async () => {
10
+ config = await testUtils.loadConfig('data-events.yaml');
11
+ });
12
+
13
+ it('populates dataEventsByExpression table', function () {
14
+ expect(_.size(config.adapter.dataEventsByExpression)).to.eq(4);
15
+ expect(config.adapter.dataEventsByExpression['data-events.tool-change-1.payload']).to.eq('tool-change-1');
16
+ expect(config.adapter.dataEventsByExpression['data-events.tool-change-1.triggers.0']).to.eq('tool-change-1');
17
+ expect(config.adapter.dataEventsByExpression['data-events.tool-offset-change-1.payload']).to.eq('tool-offset-change-1');
18
+ expect(config.adapter.dataEventsByExpression['data-events.tool-offset-change-1.triggers.0']).to.eq('tool-offset-change-1');
19
+ });
20
+
21
+ it('parses data event structure', function () {
22
+ expect(_.size(config.adapter.dataEvents)).to.eq(2);
23
+
24
+ const ev1 = config.adapter.dataEvents['tool-change-1'];
25
+ expect(ev1.name).to.eq('tool-change-1');
26
+ expect(ev1.payload.rawExpression).to.eq('{}');
27
+ expect(ev1.payload.expression).to.exist;
28
+ expect(ev1.triggers).to.have.length(1);
29
+ expect(ev1.triggers[0].rawExpression).to.eq('tool-change-1');
30
+ expect(ev1.triggers[0].expression).to.exist;
31
+ expect(ev1.triggers[0].triggerVariables).to.deep.eq(['tool-change-1']);
32
+
33
+ const ev2 = config.adapter.dataEvents['tool-offset-change-1'];
34
+ expect(ev2.name).to.eq('tool-offset-change-1');
35
+ expect(ev2.payload.rawExpression).to.eq('{ x: XgeoT1, y: YgeoT1, z: ZgeoT1, xwear: XwearT1, ywear: YwearT1, zwear: ZwearT1 }');
36
+ expect(ev2.payload.expression).to.exist;
37
+ expect(ev2.triggers).to.have.length(1);
38
+ expect(ev2.triggers[0].rawExpression).to.eq('XGeoT1-chg or YGeoT1-chg or ZGeoT1-chg or XWearT1-chg or YWearT1-chg or ZWearT1-chg');
39
+ expect(ev2.triggers[0].expression).to.exist;
40
+ expect(ev2.triggers[0].triggerVariables).to.deep.eq([
41
+ 'XGeoT1-chg', 'YGeoT1-chg', 'ZGeoT1-chg', 'XWearT1-chg', 'YWearT1-chg', 'ZWearT1-chg',
42
+ ]);
43
+ });
44
+
45
+ it('resolves payload trigger variables', function () {
46
+ const svc = config.expressionService;
47
+ expect(svc.expressionTriggers('data-events.tool-change-1.payload')).to.deep.eq([]);
48
+ expect(svc.expressionTriggers('data-events.tool-offset-change-1.payload')).to.deep.eq([
49
+ 'XgeoT1', 'YgeoT1', 'ZgeoT1', 'XwearT1', 'YwearT1', 'ZwearT1',
50
+ ]);
51
+ });
52
+
53
+ it('resolves triggered data events by trigger variable', function () {
54
+ expect(config.adapter.triggeredDataEvents('tool-change-1')).to.deep.eq(['tool-change-1']);
55
+ expect(config.adapter.triggeredDataEvents('XGeoT1-chg')).to.deep.eq(['tool-offset-change-1']);
56
+ expect(config.adapter.triggeredDataEvents('YGeoT1-chg')).to.deep.eq(['tool-offset-change-1']);
57
+ expect(config.adapter.triggeredDataEvents('ZGeoT1-chg')).to.deep.eq(['tool-offset-change-1']);
58
+ expect(config.adapter.triggeredDataEvents('XWearT1-chg')).to.deep.eq(['tool-offset-change-1']);
59
+ expect(config.adapter.triggeredDataEvents('YWearT1-chg')).to.deep.eq(['tool-offset-change-1']);
60
+ expect(config.adapter.triggeredDataEvents('ZWearT1-chg')).to.deep.eq(['tool-offset-change-1']);
61
+ });
62
+
63
+ it('resolves triggered data events by payload variable', function () {
64
+ expect(config.adapter.triggeredDataEvents('XgeoT1')).to.deep.eq(['tool-offset-change-1']);
65
+ expect(config.adapter.triggeredDataEvents('YgeoT1')).to.deep.eq(['tool-offset-change-1']);
66
+ expect(config.adapter.triggeredDataEvents('ZgeoT1')).to.deep.eq(['tool-offset-change-1']);
67
+ expect(config.adapter.triggeredDataEvents('XwearT1')).to.deep.eq(['tool-offset-change-1']);
68
+ expect(config.adapter.triggeredDataEvents('YwearT1')).to.deep.eq(['tool-offset-change-1']);
69
+ expect(config.adapter.triggeredDataEvents('ZwearT1')).to.deep.eq(['tool-offset-change-1']);
70
+ });
71
+
72
+ it('returns empty array for variables not referenced in any data event', function () {
73
+ expect(config.adapter.triggeredDataEvents('unrelated')).to.deep.eq([]);
74
+ });
75
+ });
@@ -0,0 +1,25 @@
1
+ version: 2
2
+ device: local
3
+ declare-keys:
4
+ - tool-change-1
5
+ - XgeoT1
6
+ - YgeoT1
7
+ - ZgeoT1
8
+ - XwearT1
9
+ - YwearT1
10
+ - ZwearT1
11
+ - XGeoT1-chg
12
+ - YGeoT1-chg
13
+ - ZGeoT1-chg
14
+ - XWearT1-chg
15
+ - YWearT1-chg
16
+ - ZWearT1-chg
17
+ data-events:
18
+ tool-change-1:
19
+ payload: "{}"
20
+ triggers:
21
+ - tool-change-1
22
+ tool-offset-change-1:
23
+ payload: "{ x: XgeoT1, y: YgeoT1, z: ZgeoT1, xwear: XwearT1, ywear: YwearT1, zwear: ZwearT1 }"
24
+ triggers:
25
+ - XGeoT1-chg or YGeoT1-chg or ZGeoT1-chg or XWearT1-chg or YWearT1-chg or ZWearT1-chg
@@ -8,6 +8,7 @@ declare-keys:
8
8
  - program
9
9
  - complex
10
10
  - complex2
11
+ - complex3
11
12
  variables:
12
13
  var1:
13
14
  - source: counter
@@ -23,3 +24,7 @@ variables:
23
24
  - map:
24
25
  - expression: this.subkey
25
26
  - expression: this[1]
27
+ var4:
28
+ - source: complex3
29
+ - map:
30
+ - expression: this.subkey + counter
@@ -18,3 +18,15 @@ variables:
18
18
  - expression: this + yact
19
19
  var5:
20
20
  - source: execution != 'MANUAL'
21
+ const1:
22
+ - source: 10
23
+ const2:
24
+ - source: true
25
+ const3:
26
+ - source: "'ACTIVE'"
27
+ const4:
28
+ - source: max(4, 5, 6)
29
+ var6:
30
+ - source: const1 + (const2 ? 20:30)
31
+ var7:
32
+ - source: xact + const1
@@ -54,4 +54,22 @@ describe('map full engine config file tests', function () {
54
54
  [10, 0],
55
55
  ]);
56
56
  });
57
+
58
+ it('subkey map with external ref', function () {
59
+ const engine = new EngineV2(config);
60
+ const builder = new Builder(config);
61
+ builder.build(engine);
62
+
63
+ const source = testUtils.valueSource();
64
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.var4, source);
65
+
66
+ source.sendValue('counter', 100, 0);
67
+ source.sendValue('complex3', [{ subkey: 10, xx: 15 }, { subkey: 20, xx: 30 }], 0);
68
+ source.sendValue('counter', 200, 2);
69
+
70
+ engine.validateFilter([
71
+ [[110, 120], 0],
72
+ [[210, 220], 2],
73
+ ]);
74
+ });
57
75
  });
@@ -128,6 +128,20 @@ describe('source full engine config file tests', async function () {
128
128
  [true, 0],
129
129
  ]);
130
130
  });
131
+
132
+ it('calculates constant values correctly', async function () {
133
+ const engine = new EngineV2(config);
134
+ const builder = new Builder(config);
135
+ builder.build(engine);
136
+
137
+ const source = new ValueSource();
138
+ engine.addValueSource(source);
139
+
140
+ expect(engine.getState('const1').value).to.eq(10);
141
+ expect(engine.getState('const2').value).to.eq(true);
142
+ expect(engine.getState('const3').value).to.eq('ACTIVE');
143
+ expect(engine.getState('const4').value).to.eq(6);
144
+ });
131
145
  });
132
146
 
133
147
  class ValueSource extends EventEmitter {
@@ -136,6 +136,9 @@ describe('expression transform tests', function () {
136
136
  const source = testUtils.valueSource();
137
137
  testUtils.attachEngineTransformValidator(engine, engine.variablePool.test5, source);
138
138
 
139
+ // Set by const init
140
+ expect(engine.getState('test5').value).to.eq('READY');
141
+
139
142
  source.sendValue('exec', 'ACTIVE', 0);
140
143
  source.sendValue('in-fault', false, 2);
141
144
  source.sendValue('in-fault', false, 4);
@@ -144,7 +147,7 @@ describe('expression transform tests', function () {
144
147
  source.sendValue('in-fault', false, 10);
145
148
 
146
149
  engine.validateFilter([
147
- ['READY', 0],
150
+ // ['READY', 0], // Initial value doesn't come through because chain starts with constant
148
151
  ['ACTIVE', 2],
149
152
  ['ACTIVE', 4],
150
153
  ['STOPPED', 6],