@hello.nrfcloud.com/proto-map 5.6.3 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +0 -16
  2. package/dist/generator/generateModels.js +0 -85
  3. package/dist/generator/models.js +1 -83
  4. package/dist/markdown/getCodeBlock.js +0 -26
  5. package/dist/models/check-model-rules.js +1 -91
  6. package/dist/models/models.js +5 -114
  7. package/dist/models/types.js +0 -12
  8. package/dist/senml/lwm2mToSenML.js +52 -26
  9. package/dist/senml/lwm2mToSenML.spec.js +3 -1
  10. package/dist/senml/senMLtoLwM2M.js +24 -12
  11. package/dist/senml/senMLtoLwM2M.spec.js +8 -4
  12. package/models/README.md +0 -4
  13. package/models/check-model-rules.ts +1 -90
  14. package/models/models.ts +1 -8
  15. package/models/types.ts +0 -16
  16. package/package.json +2 -3
  17. package/senml/lwm2mToSenML.spec.ts +7 -1
  18. package/senml/lwm2mToSenML.ts +42 -22
  19. package/senml/senMLtoLwM2M.spec.ts +8 -4
  20. package/senml/senMLtoLwM2M.ts +33 -16
  21. package/dist/generator/isDir.js +0 -163
  22. package/dist/generator/isDir.spec.js +0 -212
  23. package/dist/markdown/getFrontMatter.js +0 -15
  24. package/dist/models/asset_tracker_v2+AWS/examples/examples.spec.js +0 -489
  25. package/models/PCA20035+solar/README.md +0 -10
  26. package/models/PCA20035+solar/transforms/airQuality.md +0 -48
  27. package/models/PCA20035+solar/transforms/battery.md +0 -46
  28. package/models/PCA20035+solar/transforms/button.md +0 -45
  29. package/models/PCA20035+solar/transforms/deviceInfo.md +0 -72
  30. package/models/PCA20035+solar/transforms/gain.md +0 -45
  31. package/models/PCA20035+solar/transforms/geolocationFromGroundfix.md +0 -67
  32. package/models/PCA20035+solar/transforms/geolocationFromMessage.md +0 -80
  33. package/models/PCA20035+solar/transforms/humidity.md +0 -43
  34. package/models/PCA20035+solar/transforms/networkInfo.md +0 -84
  35. package/models/PCA20035+solar/transforms/pressure.md +0 -43
  36. package/models/PCA20035+solar/transforms/temperature.md +0 -43
  37. package/models/asset_tracker_v2+AWS/README.md +0 -6
  38. package/models/asset_tracker_v2+AWS/examples/examples.spec.ts +0 -229
  39. package/models/asset_tracker_v2+AWS/examples/shadow/example-1.json +0 -24
  40. package/models/asset_tracker_v2+AWS/examples/shadow/example-2.json +0 -30
  41. package/models/asset_tracker_v2+AWS/examples/shadow/example-3.json +0 -37
  42. package/models/asset_tracker_v2+AWS/examples/shadow/example-4.json +0 -48
  43. package/models/asset_tracker_v2+AWS/examples/shadow/example-5.json +0 -43
  44. package/models/asset_tracker_v2+AWS/transforms/GNSS.md +0 -66
  45. package/models/asset_tracker_v2+AWS/transforms/battery-voltage.md +0 -50
  46. package/models/asset_tracker_v2+AWS/transforms/device-info.md +0 -61
  47. package/models/asset_tracker_v2+AWS/transforms/env.md +0 -69
  48. package/models/asset_tracker_v2+AWS/transforms/fuel-gauge.md +0 -62
  49. package/models/asset_tracker_v2+AWS/transforms/roam.md +0 -100
  50. package/models/asset_tracker_v2+AWS/transforms/solar.md +0 -58
@@ -6,9 +6,6 @@ function _array_like_to_array(arr, len) {
6
6
  function _array_with_holes(arr) {
7
7
  if (Array.isArray(arr)) return arr;
8
8
  }
9
- function _array_without_holes(arr) {
10
- if (Array.isArray(arr)) return _array_like_to_array(arr);
11
- }
12
9
  function _define_property(obj, key, value) {
13
10
  if (key in obj) {
14
11
  Object.defineProperty(obj, key, {
@@ -28,9 +25,6 @@ function _iterable_to_array(iter) {
28
25
  function _non_iterable_rest() {
29
26
  throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
30
27
  }
31
- function _non_iterable_spread() {
32
- throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
33
- }
34
28
  function _object_spread(target) {
35
29
  for(var i = 1; i < arguments.length; i++){
36
30
  var source = arguments[i] != null ? arguments[i] : {};
@@ -73,9 +67,6 @@ function _object_spread_props(target, source) {
73
67
  function _to_array(arr) {
74
68
  return _array_with_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_rest();
75
69
  }
76
- function _to_consumable_array(arr) {
77
- return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread();
78
- }
79
70
  function _unsupported_iterable_to_array(o, minLen) {
80
71
  if (!o) return;
81
72
  if (typeof o === "string") return _array_like_to_array(o, minLen);
@@ -91,11 +82,6 @@ import { ResourceType } from '../lwm2m/LWM2MObjectInfo.js';
91
82
  /**
92
83
  * Convert LwM2M Object Instances to senML
93
84
  */ export var lwm2mToSenML = function(lwm2m) {
94
- return lwm2m.map(asSenML).flat().filter(function(v) {
95
- return v !== null;
96
- });
97
- };
98
- var asSenML = function(lwm2m) {
99
85
  var def = definitions[lwm2m.ObjectID];
100
86
  var i = instanceTs(lwm2m);
101
87
  var tsResourceId = timestampResources[lwm2m.ObjectID]// All registered objects must have a timestamp resource
@@ -104,19 +90,59 @@ var asSenML = function(lwm2m) {
104
90
  .filter(function(r) {
105
91
  return r[1] !== undefined;
106
92
  })), first = _Object_entries_filter[0], rest = _Object_entries_filter.slice(1);
107
- if (first === undefined) return null;
108
- var _lwm2m_ObjectInstanceID;
109
- var _obj;
110
- return [
111
- (_obj = {
93
+ if (first === undefined) return {
94
+ errors: [
95
+ new Error("No valid LwM2M object found")
96
+ ]
97
+ };
98
+ var senML = [];
99
+ var errors = [];
100
+ var resourceId = parseInt(first[0], 10);
101
+ var firstKey = toKey(def, resourceId);
102
+ if (firstKey === null) {
103
+ errors.push(new Error("Unknown ResourceID ".concat(resourceId, " for LwM2M Object ").concat(def.ObjectID, "!")));
104
+ } else {
105
+ var _lwm2m_ObjectInstanceID;
106
+ var _obj;
107
+ senML.push((_obj = {
112
108
  bn: "".concat(lwm2m.ObjectID, "/").concat((_lwm2m_ObjectInstanceID = lwm2m.ObjectInstanceID) !== null && _lwm2m_ObjectInstanceID !== void 0 ? _lwm2m_ObjectInstanceID : 0, "/"),
113
109
  n: first[0]
114
- }, _define_property(_obj, toKey(def, parseInt(first[0], 10)), first[1]), _define_property(_obj, "bt", i.getTime()), _obj)
115
- ].concat(_to_consumable_array(rest.map(function(r) {
116
- return _define_property({
117
- n: r[0]
118
- }, toKey(def, parseInt(r[0], 10)), r[1]);
119
- })));
110
+ }, _define_property(_obj, firstKey, first[1]), _define_property(_obj, "bt", i.getTime()), _obj));
111
+ }
112
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
113
+ try {
114
+ for(var _iterator = rest[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
115
+ var r = _step.value;
116
+ var resourceId1 = parseInt(r[0], 10);
117
+ var key = toKey(def, resourceId1);
118
+ if (key === null) {
119
+ errors.push(new Error("Unknown ResourceID ".concat(resourceId1, " for LwM2M Object ").concat(def.ObjectID, "!")));
120
+ } else {
121
+ senML.push(_define_property({
122
+ n: r[0]
123
+ }, key, r[1]));
124
+ }
125
+ }
126
+ } catch (err) {
127
+ _didIteratorError = true;
128
+ _iteratorError = err;
129
+ } finally{
130
+ try {
131
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
132
+ _iterator.return();
133
+ }
134
+ } finally{
135
+ if (_didIteratorError) {
136
+ throw _iteratorError;
137
+ }
138
+ }
139
+ }
140
+ if (errors.length > 0) return {
141
+ errors: errors
142
+ };
143
+ return {
144
+ senML: senML
145
+ };
120
146
  };
121
147
  var toKey = function(def, resourceId) {
122
148
  var _def_Resources_resourceId;
@@ -132,6 +158,6 @@ var toKey = function(def, resourceId) {
132
158
  case ResourceType.Opaque:
133
159
  return 'vd';
134
160
  default:
135
- throw new Error("Unknown ResourceID ".concat(resourceId, " for LwM2M Object ").concat(def.ObjectID, "!"));
161
+ return null;
136
162
  }
137
163
  };
@@ -99,6 +99,8 @@ void describe('lwm2mToSenML()', function() {
99
99
  vs: 'AES'
100
100
  }
101
101
  ];
102
- assert.deepEqual(lwm2mToSenML(lwm2m), expected);
102
+ assert.deepEqual(lwm2m.map(lwm2mToSenML).map(function(res) {
103
+ return 'senML' in res && res.senML;
104
+ }).flat(), expected);
103
105
  });
104
106
  });
@@ -53,14 +53,12 @@ function _object_spread_props(target, source) {
53
53
  import { timestampResources } from '../lwm2m/timestampResources.js';
54
54
  import { parseResourceId } from './parseResourceId.js';
55
55
  import { hasValue } from './hasValue.js';
56
- var isInfoForDifferentInstance = function(currentObject, resourceID, currentBaseTime) {
56
+ var isInfoForDifferentInstance = function(currentObject, resourceID, currentBaseTime, tsRes) {
57
57
  var _currentObject_Resources_tsRes, _currentObject_Resources;
58
58
  if (currentObject === undefined) return true;
59
59
  if (currentObject.ObjectID !== resourceID.ObjectID) return true;
60
60
  var _currentObject_ObjectInstanceID;
61
61
  if (((_currentObject_ObjectInstanceID = currentObject.ObjectInstanceID) !== null && _currentObject_ObjectInstanceID !== void 0 ? _currentObject_ObjectInstanceID : 0) !== resourceID.ObjectInstanceID) return true;
62
- var tsRes = timestampResources[resourceID.ObjectID];
63
- if (tsRes === undefined) throw new Error("Unknown LwM2M Object ID: ".concat(resourceID.ObjectID, "!"));
64
62
  if (currentBaseTime !== ((_currentObject_Resources = currentObject.Resources) === null || _currentObject_Resources === void 0 ? void 0 : (_currentObject_Resources_tsRes = _currentObject_Resources[tsRes]) === null || _currentObject_Resources_tsRes === void 0 ? void 0 : _currentObject_Resources_tsRes.getTime())) return true;
65
63
  return false;
66
64
  };
@@ -77,7 +75,7 @@ export var senMLtoLwM2M = function(senML) {
77
75
  var currentBaseName = '';
78
76
  var currentBaseTime = undefined;
79
77
  var currentObject = undefined;
80
- var items = [];
78
+ var lwm2m = [];
81
79
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
82
80
  try {
83
81
  for(var _iterator = senML[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
@@ -88,12 +86,24 @@ export var senMLtoLwM2M = function(senML) {
88
86
  var _item_n;
89
87
  var itemResourceId = "".concat(currentBaseName).concat((_item_n = item.n) !== null && _item_n !== void 0 ? _item_n : '', "/0");
90
88
  var resourceId = parseResourceId(itemResourceId);
91
- if (resourceId === null) throw new Error("Invalid resource ID: ".concat(itemResourceId));
92
- if (currentObject === undefined || currentBaseTime !== undefined && isInfoForDifferentInstance(currentObject, resourceId, currentBaseTime)) {
93
- if (currentObject !== undefined) items.push(currentObject);
94
- var tsRes = timestampResources[resourceId.ObjectID];
95
- if (tsRes === undefined) throw new Error("Unknown LwM2M Object ID: ".concat(resourceId.ObjectID, "!"));
96
- if (currentBaseTime === undefined) throw new Error("No base time defined for object!");
89
+ if (resourceId === null) {
90
+ return {
91
+ error: new Error("Invalid resource ID: ".concat(itemResourceId))
92
+ };
93
+ }
94
+ var tsRes = timestampResources[resourceId.ObjectID];
95
+ if (tsRes === undefined) {
96
+ return {
97
+ error: new Error("No timestamp resource defined for object: ".concat(resourceId.ObjectID))
98
+ };
99
+ }
100
+ if (currentObject === undefined || currentBaseTime !== undefined && isInfoForDifferentInstance(currentObject, resourceId, currentBaseTime, tsRes)) {
101
+ if (currentObject !== undefined) lwm2m.push(currentObject);
102
+ if (currentBaseTime === undefined) {
103
+ return {
104
+ error: new Error("No base time defined for object: ".concat(resourceId.ObjectID, "!"))
105
+ };
106
+ }
97
107
  currentObject = {
98
108
  ObjectID: resourceId.ObjectID,
99
109
  Resources: _define_property({}, tsRes, new Date(currentBaseTime))
@@ -121,6 +131,8 @@ export var senMLtoLwM2M = function(senML) {
121
131
  }
122
132
  }
123
133
  }
124
- if (currentObject !== undefined) items.push(currentObject);
125
- return items;
134
+ if (currentObject !== undefined) lwm2m.push(currentObject);
135
+ return {
136
+ lwm2m: lwm2m
137
+ };
126
138
  };
@@ -48,7 +48,8 @@ void describe('senMLtoLwM2M()', function() {
48
48
  }
49
49
  }
50
50
  ];
51
- assert.deepEqual(senMLtoLwM2M(input), expected);
51
+ var res = senMLtoLwM2M(input);
52
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
52
53
  });
53
54
  void it('should drop empty resources', function() {
54
55
  var input = [
@@ -101,7 +102,8 @@ void describe('senMLtoLwM2M()', function() {
101
102
  }
102
103
  }
103
104
  ];
104
- assert.deepEqual(senMLtoLwM2M(input), expected);
105
+ var res = senMLtoLwM2M(input);
106
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
105
107
  });
106
108
  void it('should ignore repeated base properties', function() {
107
109
  var input = [
@@ -160,7 +162,8 @@ void describe('senMLtoLwM2M()', function() {
160
162
  }
161
163
  }
162
164
  ];
163
- assert.deepEqual(senMLtoLwM2M(input), expected);
165
+ var res = senMLtoLwM2M(input);
166
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
164
167
  });
165
168
  void it('should handle multiple measurements for the same resource', function() {
166
169
  var input = [
@@ -221,6 +224,7 @@ void describe('senMLtoLwM2M()', function() {
221
224
  }
222
225
  }
223
226
  ];
224
- assert.deepEqual(senMLtoLwM2M(input), expected);
227
+ var res = senMLtoLwM2M(input);
228
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
225
229
  });
226
230
  });
package/models/README.md CHANGED
@@ -4,7 +4,3 @@ Models are defined in the subdirectories of this folder.
4
4
 
5
5
  A model definition consists of a `README.md` that provides a human-friendly
6
6
  description of the model.
7
-
8
- Optionally, if the device does not send senML directly,
9
- [transforms can be defined](../README.md#model-transform-definitions) that
10
- convert the payload sent by the model to senML.
@@ -1,15 +1,8 @@
1
1
  import chalk from 'chalk'
2
- import jsonata from 'jsonata'
3
2
  import assert from 'node:assert/strict'
4
3
  import { readFile, readdir, stat } from 'node:fs/promises'
5
4
  import path from 'node:path'
6
- import { FrontMatter, ModelIDRegExp } from './types.js'
7
- import { senMLtoLwM2M } from '../senml/senMLtoLwM2M.js'
8
- import { getCodeBlock } from '../markdown/getCodeBlock.js'
9
- import { getFrontMatter } from '../markdown/getFrontMatter.js'
10
- import { validateSenML } from '../senml/validateSenML.js'
11
- import { isRegisteredLwM2MObject } from '../lwm2m/isRegisteredLwM2MObject.js'
12
- import { hasValue } from '../senml/hasValue.js'
5
+ import { ModelIDRegExp } from './types.js'
13
6
  import { parseREADME } from 'markdown/parseREADME.js'
14
7
 
15
8
  console.log(chalk.gray('Models rules check'))
@@ -40,86 +33,4 @@ for (const model of await readdir(modelsDir)) {
40
33
  throw new Error(`README is not valid for ${model}!`)
41
34
  }
42
35
  console.log(chalk.green('✔'), chalk.gray(`README.md is valid`))
43
-
44
- // Validate jsonata expressions
45
- let hasTransforms = false
46
- const transformsFolder = path.join(modelDir, 'transforms')
47
- try {
48
- await stat(transformsFolder)
49
- hasTransforms = true
50
- console.log(' ', chalk.gray('Transforms:'))
51
- } catch {
52
- console.log(' ', chalk.gray('No transforms found.'))
53
- }
54
- if (hasTransforms) {
55
- for (const transform of (await readdir(transformsFolder)).filter((f) =>
56
- f.endsWith('.md'),
57
- )) {
58
- console.log(' ', chalk.white('·'), chalk.white.bold(transform))
59
- const markdown = await readFile(
60
- path.join(modelDir, 'transforms', transform),
61
- 'utf-8',
62
- )
63
-
64
- // Validate front-matter
65
- const type = getFrontMatter(markdown, FrontMatter).type
66
- console.log(' ', chalk.green('✔'), chalk.gray(`Type ${type} is valid`))
67
- const findBlock = getCodeBlock(markdown)
68
- const matchExpression = findBlock('jsonata', 'Match Expression')
69
- const transformExpression = findBlock('jsonata', 'Transform Expression')
70
- const inputExample = JSON.parse(findBlock('json', 'Input Example'))
71
- const resultExample = JSON.parse(findBlock('json', 'Result Example'))
72
-
73
- const selectResult = await jsonata(matchExpression).evaluate(inputExample)
74
- if (selectResult !== true) {
75
- throw new Error(
76
- `The select expression did not evaluate to true with the given example.`,
77
- )
78
- }
79
- console.log(
80
- ' ',
81
- chalk.green('✔'),
82
- chalk.gray('Select expression evaluated to true for the example input'),
83
- )
84
-
85
- const transformResult = await jsonata(
86
- // For testing purposes this function call result is hardcoded
87
- transformExpression.replace('$millis()', '1699999999999'),
88
- ).evaluate(inputExample)
89
-
90
- const maybeValidSenML = validateSenML(transformResult.filter(hasValue))
91
- if ('errors' in maybeValidSenML) {
92
- console.error(maybeValidSenML.errors)
93
- throw new Error('The JSONata expression must produce valid SenML')
94
- }
95
-
96
- assert.deepEqual(maybeValidSenML.value, resultExample)
97
- console.log(
98
- ' ',
99
- chalk.green('✔'),
100
- chalk.gray('Transformation result is valid SenML'),
101
- )
102
-
103
- assert.deepEqual(maybeValidSenML.value, resultExample)
104
- console.log(
105
- ' ',
106
- chalk.green('✔'),
107
- chalk.gray('The transformation result matches the example'),
108
- )
109
-
110
- // Validate
111
- for (const object of senMLtoLwM2M(maybeValidSenML.value)) {
112
- if (!isRegisteredLwM2MObject(object, console.error)) {
113
- throw new Error(
114
- 'The LwM2M object must follow LwM2M schema definition',
115
- )
116
- }
117
- console.log(
118
- ' ',
119
- chalk.green('✔'),
120
- chalk.gray('SenML object is valid LwM2M'),
121
- )
122
- }
123
- }
124
- }
125
36
  }
package/models/models.ts CHANGED
@@ -1,10 +1,7 @@
1
- import { type Transform, TransformType } from "./types.js";
2
1
  /**
3
2
  * The Model IDs defined in this repo.
4
3
  */
5
4
  export enum ModelID {
6
- PCA20035_solar = "PCA20035+solar",
7
- Asset_tracker_v2_AWS = "asset_tracker_v2+AWS",
8
5
  Kartverket_vasstandsdata = "kartverket-vasstandsdata"
9
6
  }
10
7
  export type Model = {
@@ -12,10 +9,6 @@ export type Model = {
12
9
  * The Model ID
13
10
  */
14
11
  "id": ModelID;
15
- /**
16
- * The transforms defined for this model.
17
- */
18
- "transforms": Array<Transform>;
19
12
  /**
20
13
  * Description of the Model from the README.md
21
14
  */
@@ -33,4 +26,4 @@ export type Model = {
33
26
  /**
34
27
  * The models defined for hello.nrfcloud.com
35
28
  */
36
- export const models: Readonly<Record<ModelID, Model>> = { [ModelID.PCA20035_solar]: { "id": ModelID.PCA20035_solar, "transforms": [{ "type": TransformType.Messages, "match": "appId = 'AIR_QUAL'", "transform": "[\n {\"bn\": \"14205/0/\", \"n\": \"10\", \"v\": $number(data), \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'BATTERY'", "transform": "[\n {\"bn\": \"14202/0/\", \"n\": \"0\", \"v\": $number(data), \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'BUTTON'", "transform": "[\n {\"bn\": \"14220/0/\", \"n\": \"0\", \"v\": $number(data), \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'DEVICE' and $exists(data.deviceInfo)", "transform": "[\n {\"bn\": \"14204/0/\", \"n\": \"0\", \"vs\": data.deviceInfo.imei, \"bt\": ts },\n {\"n\": \"1\", \"vs\": data.deviceInfo.iccid },\n {\"n\": \"2\", \"vs\": data.deviceInfo.modemFirmware },\n {\"n\": \"3\", \"vs\": data.deviceInfo.appVersion },\n {\"n\": \"4\", \"vs\": data.deviceInfo.board },\n {\"n\": \"5\", \"vs\": data.deviceInfo.bat }\n]" }, { "type": TransformType.Messages, "match": "appId = 'SOLAR'", "transform": "[\n {\"bn\": \"14210/0/\", \"n\": \"0\", \"v\": $number(data), \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'GROUND_FIX' and $exists(data.lat) and $exists(data.lon) and $exists(data.uncertainty) and $exists(data.fulfilledWith)", "transform": "[\n {\"bn\": \"14201/1/\", \"n\": \"0\", \"v\": data.lat, \"bt\": $millis() },\n {\"n\": \"1\", \"v\": data.lon },\n {\"n\": \"3\", \"v\": data.uncertainty },\n {\"n\": \"6\", \"vs\": data.fulfilledWith }\n]" }, { "type": TransformType.Messages, "match": "appId = 'GNSS'", "transform": "[\n {\"bn\": \"14201/0/\", \"n\": \"0\", \"v\": data.lat, \"bt\": ts },\n {\"n\": \"1\", \"v\": data.lng },\n {\"n\": \"2\", \"v\": data.alt },\n {\"n\": \"3\", \"v\": data.acc },\n {\"n\": \"4\", \"v\": data.spd },\n {\"n\": \"5\", \"v\": data.hdg },\n {\"n\": \"6\", \"vs\": \"GNSS\" }\n]" }, { "type": TransformType.Messages, "match": "appId = 'HUMID'", "transform": "[\n {\"bn\": \"14205/0/\", \"n\": \"1\", \"v\": $number(data), \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'DEVICE' and $exists(data.networkInfo)", "transform": "[\n {\"bn\": \"14203/0/\", \"n\": \"0\", \"vs\": data.networkInfo.networkMode, \"bt\": ts },\n {\"n\": \"1\", \"v\": data.networkInfo.currentBand },\n {\"n\": \"2\", \"v\": data.networkInfo.rsrp },\n {\"n\": \"3\", \"v\": data.networkInfo.areaCode },\n {\"n\": \"4\", \"v\": data.networkInfo.cellID },\n {\"n\": \"5\", \"v\": data.networkInfo.mccmnc },\n {\"n\": \"6\", \"vs\": data.networkInfo.ipAddress },\n {\"n\": \"11\", \"v\": data.networkInfo.eest }\n]" }, { "type": TransformType.Messages, "match": "appId = 'AIR_PRESS'", "transform": "[\n {\"bn\": \"14205/0/\", \"n\": \"2\", \"v\": $number(data)*10, \"bt\": ts }\n]" }, { "type": TransformType.Messages, "match": "appId = 'TEMP'", "transform": "[\n {\"bn\": \"14205/0/\", \"n\": \"0\", \"v\": $number(data), \"bt\": ts }\n]" }], "about": { "title": "Thingy:91 with Solar Shield", "description": "The Nordic Thingy:91 Solar Shield is a plug-and-play prototyping platform. Powerfoyle solar cell is mounted onto the Thingy to quickly get started exploring the endless possibilities with solar powered IoT applications and to develop products with eternal life or even battery-free products.\u200B\nThe Thingy:91 runs the asset_tracker_v2 application and sends messages to nRF Cloud using MQTT." } }, [ModelID.Asset_tracker_v2_AWS]: { "id": ModelID.Asset_tracker_v2_AWS, "transforms": [{ "type": TransformType.Shadow, "match": "$exists(state.reported.gnss)", "transform": "[\n {\"bn\": \"14201/0/\", \"n\": \"0\", \"v\": state.reported.gnss.v.lat, \"bt\": state.reported.gnss.ts },\n {\"n\": \"1\", \"v\": state.reported.gnss.v.lng },\n {\"n\": \"2\", \"v\": state.reported.gnss.v.alt },\n {\"n\": \"3\", \"v\": state.reported.gnss.v.acc },\n {\"n\": \"4\", \"v\": state.reported.gnss.v.spd },\n {\"n\": \"5\", \"v\": state.reported.gnss.v.hdg },\n {\"n\": \"6\", \"vs\": \"GNSS\" }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.bat)", "transform": "[\n {\"bn\": \"14202/0/\", \"n\": \"1\", \"v\": state.reported.bat.v/1000, \"bt\": state.reported.bat.ts }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.dev)", "transform": "[\n {\"bn\": \"14204/0/\", \"n\": \"0\", \"vs\": state.reported.dev.v.imei, \"bt\": state.reported.dev.ts },\n {\"n\": \"1\", \"vs\": state.reported.dev.v.iccid },\n {\"n\": \"2\", \"vs\": state.reported.dev.v.modV },\n {\"n\": \"3\", \"vs\": state.reported.dev.v.appV },\n {\"n\": \"4\", \"vs\": state.reported.dev.v.brdV }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.env)", "transform": "[\n {\"bn\": \"14205/0/\", \"n\": \"0\", \"v\": state.reported.env.v.temp, \"bt\": state.reported.env.ts },\n {\"n\": \"1\", \"v\": state.reported.env.v.hum },\n {\"n\": \"2\", \"v\": state.reported.env.v.atmp },\n {\"n\": \"10\", \"v\": state.reported.env.v.bsec_iaq }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.fg)", "transform": "[\n {\"bn\": \"14202/0/\", \"n\": \"0\", \"v\": state.reported.fg.v.SoC, \"bt\": state.reported.fg.ts },\n {\"n\": \"1\", \"v\": state.reported.fg.v.V/1000 },\n {\"n\": \"2\", \"v\": state.reported.fg.v.I },\n {\"n\": \"3\", \"v\": state.reported.fg.v.T = null ? null : state.reported.fg.v.T/10 },\n {\"n\": \"4\", \"v\": state.reported.fg.v.TTF },\n {\"n\": \"5\", \"v\": state.reported.fg.v.TTE }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.roam)", "transform": "[\n {\"bn\": \"14203/0/\", \"n\": \"0\", \"vs\": state.reported.roam.v.nw, \"bt\": state.reported.roam.ts },\n {\"n\": \"1\", \"v\": state.reported.roam.v.band },\n {\"bn\": \"14203/0/\", \"n\": \"2\", \"v\": state.reported.roam.v.rsrp, \"bt\": state.reported.roam.ts },\n {\"n\": \"3\", \"v\": state.reported.roam.v.area },\n {\"n\": \"4\", \"v\": state.reported.roam.v.cell },\n {\"n\": \"5\", \"v\": state.reported.roam.v.mccmnc },\n {\"n\": \"6\", \"vs\": state.reported.roam.v.ip },\n {\"bn\": \"14203/0/\", \"n\": \"11\", \"v\": state.reported.roam.v.eest, \"bt\": state.reported.roam.ts }\n]" }, { "type": TransformType.Shadow, "match": "$exists(state.reported.sol)", "transform": "[\n {\"bn\": \"14210/0/\", \"n\": \"0\", \"v\": state.reported.sol.v.gain, \"bt\": state.reported.sol.ts },\n {\"n\": \"1\", \"v\": state.reported.sol.v.bat }\n]" }], "about": { "title": "asset_tracker_v2 on AWS", "description": "This implements the conversion for the asset_tracker_v2 message protocol when connected to AWS IoT." } }, [ModelID.Kartverket_vasstandsdata]: { "id": ModelID.Kartverket_vasstandsdata, "transforms": [], "about": { "title": "Kartverket Vasstandsdata", "description": "A simulated device reporting the current sea level as provided by the Kartverket's (Norwegian Mapping Authority) API for vasstandsdata (API for water level data).\nReports sea water level using the Object 14230.\nThe data is licensed by the Norwegian Mapping Authority\u2019s under the Creative Commons Attribution 4.0 International (CC BY 4.0) license." } } } as const;
29
+ export const models: Readonly<Record<ModelID, Model>> = { [ModelID.Kartverket_vasstandsdata]: { "id": ModelID.Kartverket_vasstandsdata, "about": { "title": "Kartverket Vasstandsdata", "description": "A simulated device reporting the current sea level as provided by the Kartverket's (Norwegian Mapping Authority) API for vasstandsdata (API for water level data).\nReports sea water level using the Object 14230.\nThe data is licensed by the Norwegian Mapping Authority\u2019s under the Creative Commons Attribution 4.0 International (CC BY 4.0) license." } } } as const;
package/models/types.ts CHANGED
@@ -1,17 +1 @@
1
- import { Type } from '@sinclair/typebox'
2
-
3
1
  export const ModelIDRegExp = /^[A-Za-z0-9+_-]+$/
4
-
5
- export enum TransformType {
6
- Shadow = 'shadow',
7
- Messages = 'messages',
8
- }
9
- export type Transform = {
10
- type: TransformType
11
- match: string
12
- transform: string
13
- }
14
-
15
- export const FrontMatter = Type.Object({
16
- type: Type.Union([Type.Literal('shadow'), Type.Literal('messages')]),
17
- })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hello.nrfcloud.com/proto-map",
3
- "version": "5.6.3",
3
+ "version": "7.0.0",
4
4
  "description": "Documents the communication protocol between devices, the hello.nrfcloud.com/map backend and web application",
5
5
  "type": "module",
6
6
  "exports": {
@@ -105,7 +105,6 @@
105
105
  ],
106
106
  "peerDependencies": {
107
107
  "@sinclair/typebox": "^0.32.22",
108
- "ajv": "^8.12.0",
109
- "jsonata": "^2.0.4"
108
+ "ajv": "^8.12.0"
110
109
  }
111
110
  }
@@ -69,6 +69,12 @@ void describe('lwm2mToSenML()', () => {
69
69
  { n: '1', vs: 'AES' },
70
70
  ]
71
71
 
72
- assert.deepEqual(lwm2mToSenML(lwm2m), expected)
72
+ assert.deepEqual(
73
+ lwm2m
74
+ .map(lwm2mToSenML)
75
+ .map((res) => 'senML' in res && res.senML)
76
+ .flat(),
77
+ expected,
78
+ )
73
79
  })
74
80
  })
@@ -9,14 +9,8 @@ import { ResourceType, type LWM2MObjectInfo } from '../lwm2m/LWM2MObjectInfo.js'
9
9
  * Convert LwM2M Object Instances to senML
10
10
  */
11
11
  export const lwm2mToSenML = (
12
- lwm2m: Array<LwM2MObjectInstance<any>>,
13
- ): SenMLType =>
14
- lwm2m
15
- .map(asSenML)
16
- .flat()
17
- .filter((v) => v !== null) as SenMLType
18
-
19
- const asSenML = (lwm2m: LwM2MObjectInstance<any>): SenMLType | null => {
12
+ lwm2m: LwM2MObjectInstance<any>,
13
+ ): { senML: SenMLType } | { errors: Array<Error> } => {
20
14
  const def = definitions[lwm2m.ObjectID]
21
15
  const i = instanceTs(lwm2m)
22
16
  const tsResourceId = timestampResources[lwm2m.ObjectID] as number // All registered objects must have a timestamp resource
@@ -27,22 +21,50 @@ const asSenML = (lwm2m: LwM2MObjectInstance<any>): SenMLType | null => {
27
21
  // Filter out undefined values (and timestamp resource)
28
22
  .filter((r): r is [string, LwM2MResourceValue] => r[1] !== undefined)
29
23
 
30
- if (first === undefined) return null
31
- return [
32
- {
24
+ if (first === undefined)
25
+ return { errors: [new Error(`No valid LwM2M object found`)] }
26
+
27
+ const senML: SenMLType = []
28
+ const errors: Array<Error> = []
29
+ const resourceId = parseInt(first[0], 10)
30
+ const firstKey = toKey(def, resourceId)
31
+ if (firstKey === null) {
32
+ errors.push(
33
+ new Error(
34
+ `Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
35
+ ),
36
+ )
37
+ } else {
38
+ senML.push({
33
39
  bn: `${lwm2m.ObjectID}/${lwm2m.ObjectInstanceID ?? 0}/`,
34
40
  n: first[0],
35
- [toKey(def, parseInt(first[0], 10))]: first[1],
41
+ [firstKey]: first[1],
36
42
  bt: i.getTime(),
37
- },
38
- ...rest.map((r) => ({
39
- n: r[0],
40
- [toKey(def, parseInt(r[0], 10))]: r[1],
41
- })),
42
- ]
43
+ })
44
+ }
45
+
46
+ for (const r of rest) {
47
+ const resourceId = parseInt(r[0], 10)
48
+ const key = toKey(def, resourceId)
49
+ if (key === null) {
50
+ errors.push(
51
+ new Error(
52
+ `Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
53
+ ),
54
+ )
55
+ } else {
56
+ senML.push({
57
+ n: r[0],
58
+ [key]: r[1],
59
+ })
60
+ }
61
+ }
62
+
63
+ if (errors.length > 0) return { errors }
64
+ return { senML }
43
65
  }
44
66
 
45
- const toKey = (def: LWM2MObjectInfo, resourceId: number) => {
67
+ const toKey = (def: LWM2MObjectInfo, resourceId: number): string | null => {
46
68
  switch (def.Resources[resourceId]?.Type) {
47
69
  case ResourceType.String:
48
70
  return 'vs'
@@ -55,8 +77,6 @@ const toKey = (def: LWM2MObjectInfo, resourceId: number) => {
55
77
  case ResourceType.Opaque:
56
78
  return 'vd'
57
79
  default:
58
- throw new Error(
59
- `Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
60
- )
80
+ return null
61
81
  }
62
82
  }
@@ -50,7 +50,8 @@ void describe('senMLtoLwM2M()', () => {
50
50
  },
51
51
  },
52
52
  ]
53
- assert.deepEqual(senMLtoLwM2M(input), expected)
53
+ const res = senMLtoLwM2M(input)
54
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
54
55
  })
55
56
 
56
57
  void it('should drop empty resources', () => {
@@ -104,7 +105,8 @@ void describe('senMLtoLwM2M()', () => {
104
105
  },
105
106
  },
106
107
  ]
107
- assert.deepEqual(senMLtoLwM2M(input), expected)
108
+ const res = senMLtoLwM2M(input)
109
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
108
110
  })
109
111
 
110
112
  void it('should ignore repeated base properties', () => {
@@ -134,7 +136,8 @@ void describe('senMLtoLwM2M()', () => {
134
136
  },
135
137
  },
136
138
  ]
137
- assert.deepEqual(senMLtoLwM2M(input), expected)
139
+ const res = senMLtoLwM2M(input)
140
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
138
141
  })
139
142
 
140
143
  void it('should handle multiple measurements for the same resource', () => {
@@ -176,6 +179,7 @@ void describe('senMLtoLwM2M()', () => {
176
179
  },
177
180
  },
178
181
  ]
179
- assert.deepEqual(senMLtoLwM2M(input), expected)
182
+ const res = senMLtoLwM2M(input)
183
+ assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
180
184
  })
181
185
  })
@@ -42,14 +42,12 @@ const isInfoForDifferentInstance = (
42
42
  currentObject: LwM2MObjectInstance,
43
43
  resourceID: ResourceID,
44
44
  currentBaseTime: number,
45
+ tsRes: number,
45
46
  ): boolean => {
46
47
  if (currentObject === undefined) return true
47
48
  if (currentObject.ObjectID !== resourceID.ObjectID) return true
48
49
  if ((currentObject.ObjectInstanceID ?? 0) !== resourceID.ObjectInstanceID)
49
50
  return true
50
- const tsRes = timestampResources[resourceID.ObjectID]
51
- if (tsRes === undefined)
52
- throw new Error(`Unknown LwM2M Object ID: ${resourceID.ObjectID}!`)
53
51
  if (
54
52
  currentBaseTime !==
55
53
  (currentObject.Resources?.[tsRes] as Date | undefined)?.getTime()
@@ -70,11 +68,13 @@ const getValue = (
70
68
  if ('vd' in measurement) return measurement.vd
71
69
  return undefined
72
70
  }
73
- export const senMLtoLwM2M = (senML: SenMLType): Array<LwM2MObjectInstance> => {
71
+ export const senMLtoLwM2M = (
72
+ senML: SenMLType,
73
+ ): { lwm2m: Array<LwM2MObjectInstance> } | { error: Error } => {
74
74
  let currentBaseName: string = ''
75
75
  let currentBaseTime: number | undefined = undefined
76
76
  let currentObject: LwM2MObjectInstance | undefined = undefined
77
- const items: LwM2MObjectInstance[] = []
77
+ const lwm2m: Array<LwM2MObjectInstance> = []
78
78
 
79
79
  for (const item of senML) {
80
80
  if ('bn' in item && item.bn !== undefined) currentBaseName = item.bn
@@ -82,20 +82,37 @@ export const senMLtoLwM2M = (senML: SenMLType): Array<LwM2MObjectInstance> => {
82
82
  if (!hasValue(item)) continue
83
83
  const itemResourceId = `${currentBaseName}${item.n ?? ''}/0`
84
84
  const resourceId = parseResourceId(itemResourceId)
85
- if (resourceId === null)
86
- throw new Error(`Invalid resource ID: ${itemResourceId}`)
85
+ if (resourceId === null) {
86
+ return { error: new Error(`Invalid resource ID: ${itemResourceId}`) }
87
+ }
88
+
89
+ const tsRes = timestampResources[resourceId.ObjectID]
90
+ if (tsRes === undefined) {
91
+ return {
92
+ error: new Error(
93
+ `No timestamp resource defined for object: ${resourceId.ObjectID}`,
94
+ ),
95
+ }
96
+ }
87
97
 
88
98
  if (
89
99
  currentObject === undefined ||
90
100
  (currentBaseTime !== undefined &&
91
- isInfoForDifferentInstance(currentObject, resourceId, currentBaseTime))
101
+ isInfoForDifferentInstance(
102
+ currentObject,
103
+ resourceId,
104
+ currentBaseTime,
105
+ tsRes,
106
+ ))
92
107
  ) {
93
- if (currentObject !== undefined) items.push(currentObject)
94
- const tsRes = timestampResources[resourceId.ObjectID]
95
- if (tsRes === undefined)
96
- throw new Error(`Unknown LwM2M Object ID: ${resourceId.ObjectID}!`)
97
- if (currentBaseTime === undefined)
98
- throw new Error(`No base time defined for object!`)
108
+ if (currentObject !== undefined) lwm2m.push(currentObject)
109
+ if (currentBaseTime === undefined) {
110
+ return {
111
+ error: new Error(
112
+ `No base time defined for object: ${resourceId.ObjectID}!`,
113
+ ),
114
+ }
115
+ }
99
116
  currentObject = {
100
117
  ObjectID: resourceId.ObjectID,
101
118
  Resources: {
@@ -115,7 +132,7 @@ export const senMLtoLwM2M = (senML: SenMLType): Array<LwM2MObjectInstance> => {
115
132
  }
116
133
  }
117
134
  }
118
- if (currentObject !== undefined) items.push(currentObject)
135
+ if (currentObject !== undefined) lwm2m.push(currentObject)
119
136
 
120
- return items
137
+ return { lwm2m }
121
138
  }