@hello.nrfcloud.com/proto-map 4.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.
- package/LICENSE +29 -0
- package/README.md +122 -0
- package/dist/generator/addDocBlock.js +5 -0
- package/dist/generator/generateLwM2MDefinitions.js +82 -0
- package/dist/generator/generateLwM2MDefinitions.spec.js +91 -0
- package/dist/generator/generateLwm2mTimestampResources.js +69 -0
- package/dist/generator/generateModels.js +142 -0
- package/dist/generator/generateType.js +90 -0
- package/dist/generator/generateValidator.js +132 -0
- package/dist/generator/generateValidators.js +63 -0
- package/dist/generator/isDir.js +163 -0
- package/dist/generator/isDir.spec.js +212 -0
- package/dist/generator/lwm2m.js +106 -0
- package/dist/generator/models.js +306 -0
- package/dist/generator/printNode.js +8 -0
- package/dist/generator/tokenizeName.js +5 -0
- package/dist/generator/tokenizeName.spec.js +98 -0
- package/dist/generator/types.js +140 -0
- package/dist/lwm2m/LWM2MObjectDefinition.js +88 -0
- package/dist/lwm2m/LWM2MObjectInfo.js +9 -0
- package/dist/lwm2m/LwM2MObject.d.js +1 -0
- package/dist/lwm2m/LwM2MObjectID.js +57 -0
- package/dist/lwm2m/ParsedLwM2MObjectDefinition.js +1 -0
- package/dist/lwm2m/check-lwm2m-rules.js +480 -0
- package/dist/lwm2m/definitions.js +596 -0
- package/dist/lwm2m/fromXML2JSON.js +194 -0
- package/dist/lwm2m/instanceTs.js +9 -0
- package/dist/lwm2m/instanceTs.spec.js +16 -0
- package/dist/lwm2m/isRegisteredLwM2MObject.js +3 -0
- package/dist/lwm2m/isRegisteredLwM2MObject.spec.js +45 -0
- package/dist/lwm2m/object/14201.d.js +5 -0
- package/dist/lwm2m/object/14202.d.js +5 -0
- package/dist/lwm2m/object/14203.d.js +5 -0
- package/dist/lwm2m/object/14204.d.js +5 -0
- package/dist/lwm2m/object/14205.d.js +5 -0
- package/dist/lwm2m/object/14210.d.js +5 -0
- package/dist/lwm2m/object/14220.d.js +5 -0
- package/dist/lwm2m/object/14230.d.js +5 -0
- package/dist/lwm2m/object/validate14201.js +18 -0
- package/dist/lwm2m/object/validate14202.js +17 -0
- package/dist/lwm2m/object/validate14203.js +19 -0
- package/dist/lwm2m/object/validate14204.js +17 -0
- package/dist/lwm2m/object/validate14205.js +15 -0
- package/dist/lwm2m/object/validate14210.js +13 -0
- package/dist/lwm2m/object/validate14220.js +12 -0
- package/dist/lwm2m/object/validate14230.js +13 -0
- package/dist/lwm2m/objects.js +8 -0
- package/dist/lwm2m/parseRangeEnumeration.js +20 -0
- package/dist/lwm2m/parseRangeEnumeration.spec.js +27 -0
- package/dist/lwm2m/resourceType.js +12 -0
- package/dist/lwm2m/timestampResources.js +12 -0
- package/dist/lwm2m/unwrapNestedArray.js +114 -0
- package/dist/lwm2m/unwrapNestedArray.spec.js +374 -0
- package/dist/lwm2m/validate.js +14 -0
- package/dist/lwm2m/validation.js +146 -0
- package/dist/lwm2m/validators.js +20 -0
- package/dist/markdown/getCodeBlock.js +74 -0
- package/dist/markdown/getFrontMatter.js +15 -0
- package/dist/markdown/parseREADME.js +19 -0
- package/dist/models/asset_tracker_v2+AWS/examples/examples.spec.js +489 -0
- package/dist/models/check-model-rules.js +137 -0
- package/dist/models/models.js +137 -0
- package/dist/models/types.js +13 -0
- package/dist/senml/SenMLSchema.js +79 -0
- package/dist/senml/SenMLSchema.spec.js +23 -0
- package/dist/senml/hasValue.js +8 -0
- package/dist/senml/hasValue.spec.js +103 -0
- package/dist/senml/lwm2mToSenML.js +137 -0
- package/dist/senml/lwm2mToSenML.spec.js +104 -0
- package/dist/senml/parseResourceId.js +58 -0
- package/dist/senml/parseResourceId.spec.js +13 -0
- package/dist/senml/senMLtoLwM2M.js +126 -0
- package/dist/senml/senMLtoLwM2M.spec.js +226 -0
- package/dist/senml/validateSenML.js +6 -0
- package/dist/senml/validateSenML.spec.js +31 -0
- package/export.js +13 -0
- package/index.d.ts +14 -0
- package/lwm2m/14201.xml +94 -0
- package/lwm2m/14202.xml +84 -0
- package/lwm2m/14203.xml +104 -0
- package/lwm2m/14204.xml +84 -0
- package/lwm2m/14205.xml +64 -0
- package/lwm2m/14210.xml +44 -0
- package/lwm2m/14220.xml +34 -0
- package/lwm2m/14230.xml +44 -0
- package/lwm2m/LWM2M-v1_1.xsd +168 -0
- package/lwm2m/LWM2MObjectDefinition.ts +84 -0
- package/lwm2m/LWM2MObjectInfo.ts +42 -0
- package/lwm2m/LwM2MObject.d.ts +19 -0
- package/lwm2m/LwM2MObjectID.ts +73 -0
- package/lwm2m/ParsedLwM2MObjectDefinition.ts +28 -0
- package/lwm2m/check-lwm2m-rules.ts +160 -0
- package/lwm2m/definitions.ts +278 -0
- package/lwm2m/format.sh +3 -0
- package/lwm2m/fromXML2JSON.ts +44 -0
- package/lwm2m/instanceTs.spec.ts +19 -0
- package/lwm2m/instanceTs.ts +10 -0
- package/lwm2m/isRegisteredLwM2MObject.spec.ts +48 -0
- package/lwm2m/isRegisteredLwM2MObject.ts +4 -0
- package/lwm2m/object/14201.d.ts +73 -0
- package/lwm2m/object/14202.d.ts +59 -0
- package/lwm2m/object/14203.d.ts +67 -0
- package/lwm2m/object/14204.d.ts +55 -0
- package/lwm2m/object/14205.d.ts +43 -0
- package/lwm2m/object/14210.d.ts +31 -0
- package/lwm2m/object/14220.d.ts +25 -0
- package/lwm2m/object/14230.d.ts +31 -0
- package/lwm2m/object/validate14201.ts +10 -0
- package/lwm2m/object/validate14202.ts +10 -0
- package/lwm2m/object/validate14203.ts +10 -0
- package/lwm2m/object/validate14204.ts +10 -0
- package/lwm2m/object/validate14205.ts +10 -0
- package/lwm2m/object/validate14210.ts +10 -0
- package/lwm2m/object/validate14220.ts +10 -0
- package/lwm2m/object/validate14230.ts +10 -0
- package/lwm2m/objects.ts +16 -0
- package/lwm2m/parseRangeEnumeration.spec.ts +34 -0
- package/lwm2m/parseRangeEnumeration.ts +29 -0
- package/lwm2m/resourceType.ts +11 -0
- package/lwm2m/timestampResources.ts +4 -0
- package/lwm2m/unwrapNestedArray.spec.ts +241 -0
- package/lwm2m/unwrapNestedArray.ts +27 -0
- package/lwm2m/validate.ts +30 -0
- package/lwm2m/validation.ts +120 -0
- package/lwm2m/validators.ts +21 -0
- package/models/PCA20035+solar/README.md +10 -0
- package/models/PCA20035+solar/transforms/airQuality.md +48 -0
- package/models/PCA20035+solar/transforms/battery.md +46 -0
- package/models/PCA20035+solar/transforms/button.md +45 -0
- package/models/PCA20035+solar/transforms/deviceInfo.md +72 -0
- package/models/PCA20035+solar/transforms/gain.md +45 -0
- package/models/PCA20035+solar/transforms/geolocationFromGroundfix.md +67 -0
- package/models/PCA20035+solar/transforms/geolocationFromMessage.md +80 -0
- package/models/PCA20035+solar/transforms/humidity.md +43 -0
- package/models/PCA20035+solar/transforms/networkInfo.md +84 -0
- package/models/PCA20035+solar/transforms/pressure.md +43 -0
- package/models/PCA20035+solar/transforms/temperature.md +43 -0
- package/models/README.md +10 -0
- package/models/asset_tracker_v2+AWS/README.md +6 -0
- package/models/asset_tracker_v2+AWS/examples/examples.spec.ts +229 -0
- package/models/asset_tracker_v2+AWS/examples/shadow/example-1.json +24 -0
- package/models/asset_tracker_v2+AWS/examples/shadow/example-2.json +30 -0
- package/models/asset_tracker_v2+AWS/examples/shadow/example-3.json +37 -0
- package/models/asset_tracker_v2+AWS/examples/shadow/example-4.json +48 -0
- package/models/asset_tracker_v2+AWS/examples/shadow/example-5.json +43 -0
- package/models/asset_tracker_v2+AWS/transforms/GNSS.md +66 -0
- package/models/asset_tracker_v2+AWS/transforms/battery-voltage.md +50 -0
- package/models/asset_tracker_v2+AWS/transforms/device-info.md +61 -0
- package/models/asset_tracker_v2+AWS/transforms/env.md +69 -0
- package/models/asset_tracker_v2+AWS/transforms/fuel-gauge.md +62 -0
- package/models/asset_tracker_v2+AWS/transforms/roam.md +100 -0
- package/models/asset_tracker_v2+AWS/transforms/solar.md +58 -0
- package/models/check-model-rules.ts +125 -0
- package/models/kartverket-vasstandsdata/README.md +13 -0
- package/models/models.ts +36 -0
- package/models/types.ts +17 -0
- package/package.json +111 -0
- package/senml/SenMLSchema.spec.ts +21 -0
- package/senml/SenMLSchema.ts +74 -0
- package/senml/hasValue.spec.ts +19 -0
- package/senml/hasValue.ts +12 -0
- package/senml/lwm2mToSenML.spec.ts +74 -0
- package/senml/lwm2mToSenML.ts +62 -0
- package/senml/parseResourceId.spec.ts +13 -0
- package/senml/parseResourceId.ts +23 -0
- package/senml/senMLtoLwM2M.spec.ts +181 -0
- package/senml/senMLtoLwM2M.ts +121 -0
- package/senml/validateSenML.spec.ts +16 -0
- package/senml/validateSenML.ts +8 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import jsonata from 'jsonata'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
5
|
+
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'
|
|
13
|
+
import { parseREADME } from 'markdown/parseREADME.js'
|
|
14
|
+
|
|
15
|
+
console.log(chalk.gray('Models rules check'))
|
|
16
|
+
console.log('')
|
|
17
|
+
const modelsDir = path.join(process.cwd(), 'models')
|
|
18
|
+
for (const model of await readdir(modelsDir)) {
|
|
19
|
+
const modelDir = path.join(modelsDir, model)
|
|
20
|
+
if (!(await stat(modelDir)).isDirectory()) continue
|
|
21
|
+
console.log(chalk.white('·'), chalk.white.bold(model))
|
|
22
|
+
assert.match(
|
|
23
|
+
model,
|
|
24
|
+
ModelIDRegExp,
|
|
25
|
+
'Model identifiers must consist of numbers, letters, dash, plus, and underscore only',
|
|
26
|
+
)
|
|
27
|
+
console.log(chalk.green('✔'), chalk.gray('Model name is correct'))
|
|
28
|
+
|
|
29
|
+
// A README.md should exist
|
|
30
|
+
try {
|
|
31
|
+
await stat(path.join(modelDir, 'README.md'))
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`No README.md defined for model ${model}!`)
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk.green('✔'), chalk.gray(`README.md exists`))
|
|
36
|
+
try {
|
|
37
|
+
parseREADME(await readFile(path.join(modelDir, 'README.md'), 'utf-8'))
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(err)
|
|
40
|
+
throw new Error(`README is not valid for ${model}!`)
|
|
41
|
+
}
|
|
42
|
+
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Kartverket Vasstandsdata
|
|
2
|
+
|
|
3
|
+
A simulated device reporting the current sea level as provided by the
|
|
4
|
+
[Kartverket](https://www.kartverket.no/)'s (Norwegian Mapping Authority)
|
|
5
|
+
[API for vasstandsdata](https://api.sehavniva.no/tideapi_no.html) (API for water
|
|
6
|
+
level data).
|
|
7
|
+
|
|
8
|
+
Reports sea water level using the Object [`14230`](../../lwm2m/14230.xml).
|
|
9
|
+
|
|
10
|
+
The data is licensed by the
|
|
11
|
+
[Norwegian Mapping Authority](https://www.kartverket.no/)’s under the
|
|
12
|
+
[Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/)
|
|
13
|
+
license.
|
package/models/models.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Transform, TransformType } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Model IDs defined in this repo.
|
|
4
|
+
*/
|
|
5
|
+
export enum ModelID {
|
|
6
|
+
PCA20035_solar = "PCA20035+solar",
|
|
7
|
+
Asset_tracker_v2_AWS = "asset_tracker_v2+AWS",
|
|
8
|
+
Kartverket_vasstandsdata = "kartverket-vasstandsdata"
|
|
9
|
+
}
|
|
10
|
+
export type Model = {
|
|
11
|
+
/**
|
|
12
|
+
* The Model ID
|
|
13
|
+
*/
|
|
14
|
+
"id": ModelID;
|
|
15
|
+
/**
|
|
16
|
+
* The transforms defined for this model.
|
|
17
|
+
*/
|
|
18
|
+
"transforms": Array<Transform>;
|
|
19
|
+
/**
|
|
20
|
+
* Description of the Model from the README.md
|
|
21
|
+
*/
|
|
22
|
+
"about": {
|
|
23
|
+
/**
|
|
24
|
+
* The text of the H1 headline
|
|
25
|
+
*/
|
|
26
|
+
"title": string;
|
|
27
|
+
/**
|
|
28
|
+
* The text of the paragraphs following the H1 headline
|
|
29
|
+
*/
|
|
30
|
+
"description": string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The models defined for hello.nrfcloud.com
|
|
35
|
+
*/
|
|
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;
|
package/models/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox'
|
|
2
|
+
|
|
3
|
+
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
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hello.nrfcloud.com/proto-map",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Documents the communication protocol between devices, the hello.nrfcloud.com/map backend and web application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./export.js",
|
|
10
|
+
"node": "./export.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/hello-nrfcloud/proto-map.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/hello-nrfcloud/proto-map/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://hello.nrfcloud.com",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"nordicsemiconductor",
|
|
23
|
+
"cellular-iot",
|
|
24
|
+
"hello-nrfcloud",
|
|
25
|
+
"hello-nrfcloud-map"
|
|
26
|
+
],
|
|
27
|
+
"author": "Nordic Semiconductor ASA | nordicsemi.no",
|
|
28
|
+
"license": "BSD-3-Clause",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0",
|
|
31
|
+
"npm": ">=9.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"prepare": "husky",
|
|
35
|
+
"test": "find ./ -type f -name \\*.spec.ts -not -path './node_modules/*' | xargs npx tsx --test --test-reporter spec",
|
|
36
|
+
"prepublishOnly": "./compile.sh"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@bifravst/eslint-config-typescript": "6.0.11",
|
|
40
|
+
"@bifravst/prettier-config": "1.0.0",
|
|
41
|
+
"@commitlint/config-conventional": "19.1.0",
|
|
42
|
+
"@swc/cli": "0.3.10",
|
|
43
|
+
"@swc/core": "1.4.8",
|
|
44
|
+
"@types/node": "20.11.27",
|
|
45
|
+
"@types/xml2js": "0.4.14",
|
|
46
|
+
"chalk": "5.3.0",
|
|
47
|
+
"husky": "9.0.11",
|
|
48
|
+
"lint-staged": "15.2.2",
|
|
49
|
+
"prettier-plugin-organize-imports": "3.2.4",
|
|
50
|
+
"remark": "15.0.1",
|
|
51
|
+
"remark-frontmatter": "5.0.0",
|
|
52
|
+
"tsmatchers": "5.0.2",
|
|
53
|
+
"tsx": "4.7.1",
|
|
54
|
+
"xml2js": "0.6.2",
|
|
55
|
+
"yaml": "2.4.1"
|
|
56
|
+
},
|
|
57
|
+
"lint-staged": {
|
|
58
|
+
"*.{ts,tsx}": [
|
|
59
|
+
"prettier --write",
|
|
60
|
+
"eslint"
|
|
61
|
+
],
|
|
62
|
+
"*.{md,json,yaml,yml,html}": [
|
|
63
|
+
"prettier --write"
|
|
64
|
+
],
|
|
65
|
+
"*.xml": [
|
|
66
|
+
"./lwm2m/format.sh"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"prettier": "@bifravst/prettier-config",
|
|
70
|
+
"release": {
|
|
71
|
+
"branches": [
|
|
72
|
+
"saga",
|
|
73
|
+
{
|
|
74
|
+
"name": "!(saga|v[0-9].[0-9].x|*_*|*/*)",
|
|
75
|
+
"prerelease": true
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"remoteTags": true,
|
|
79
|
+
"plugins": [
|
|
80
|
+
"@semantic-release/commit-analyzer",
|
|
81
|
+
"@semantic-release/release-notes-generator",
|
|
82
|
+
"@semantic-release/npm",
|
|
83
|
+
[
|
|
84
|
+
"@semantic-release/github",
|
|
85
|
+
{
|
|
86
|
+
"successComment": false,
|
|
87
|
+
"failTitle": false
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"publishConfig": {
|
|
93
|
+
"access": "public"
|
|
94
|
+
},
|
|
95
|
+
"files": [
|
|
96
|
+
"package-lock.json",
|
|
97
|
+
"LICENSE",
|
|
98
|
+
"README.md",
|
|
99
|
+
"dist",
|
|
100
|
+
"export.js",
|
|
101
|
+
"index.d.ts",
|
|
102
|
+
"senml",
|
|
103
|
+
"models",
|
|
104
|
+
"lwm2m"
|
|
105
|
+
],
|
|
106
|
+
"dependencies": {
|
|
107
|
+
"@sinclair/typebox": "0.32.15",
|
|
108
|
+
"ajv": "8.12.0",
|
|
109
|
+
"jsonata": "2.0.4"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { SenML, type SenMLType } from './SenMLSchema.js'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { validate } from '../validate.js'
|
|
5
|
+
|
|
6
|
+
void describe('SenMLType', () => {
|
|
7
|
+
void it('it should validate a SenML payload', () => {
|
|
8
|
+
const example: SenMLType = [
|
|
9
|
+
{
|
|
10
|
+
bn: '/14201/0/',
|
|
11
|
+
n: '0',
|
|
12
|
+
v: 33.98755678796222,
|
|
13
|
+
bt: 1698155694999,
|
|
14
|
+
},
|
|
15
|
+
{ n: '1', v: -84.506132079174634 },
|
|
16
|
+
]
|
|
17
|
+
const res = validate(SenML)(example)
|
|
18
|
+
assert.equal('errors' in res, false)
|
|
19
|
+
assert.deepEqual('value' in res && res.value, example)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Type, type Static } from '@sinclair/typebox'
|
|
2
|
+
|
|
3
|
+
const ResourceIDPart = Type.RegExp(/^[0-9/]+$/, {
|
|
4
|
+
title: 'ResourceIDPart',
|
|
5
|
+
description:
|
|
6
|
+
'Combines `bn` and `n` to a fully qualified resource identifier in the form of `/<object ID>/<object instance ID>/<resource ID>/0`. (Multiple resource instances are not supported right now.).',
|
|
7
|
+
examples: ['/', '/14201/0/', '5'],
|
|
8
|
+
})
|
|
9
|
+
const BaseValue = Type.Number({ title: 'Base Value' })
|
|
10
|
+
const Value = Type.Number({ title: 'Value' })
|
|
11
|
+
const Time = Type.Integer({ minimum: 0, title: 'Time' })
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defines a SenML type with some unsupported elements removed: Sum, Base Sum, Update Time
|
|
15
|
+
*
|
|
16
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8428
|
|
17
|
+
*/
|
|
18
|
+
export const Measurement = Type.Intersect(
|
|
19
|
+
[
|
|
20
|
+
Type.Union([
|
|
21
|
+
Type.Object({
|
|
22
|
+
n: ResourceIDPart,
|
|
23
|
+
}),
|
|
24
|
+
Type.Object({
|
|
25
|
+
bn: ResourceIDPart,
|
|
26
|
+
n: ResourceIDPart,
|
|
27
|
+
blv: Type.String({
|
|
28
|
+
minLength: 1,
|
|
29
|
+
description: 'The LwM2M object version used',
|
|
30
|
+
default: '1.0',
|
|
31
|
+
}),
|
|
32
|
+
bt: Type.Optional(Time),
|
|
33
|
+
}),
|
|
34
|
+
]),
|
|
35
|
+
// Value combinations
|
|
36
|
+
Type.Union([
|
|
37
|
+
Type.Union([
|
|
38
|
+
Type.Object({
|
|
39
|
+
bv: BaseValue,
|
|
40
|
+
}),
|
|
41
|
+
Type.Object({
|
|
42
|
+
bv: BaseValue,
|
|
43
|
+
v: Value,
|
|
44
|
+
}),
|
|
45
|
+
Type.Object({
|
|
46
|
+
// Value can be undefined, if it is not mandatory
|
|
47
|
+
v: Type.Optional(Value),
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
Type.Object({
|
|
51
|
+
vs: Type.String({ minLength: 1, title: 'String Value' }),
|
|
52
|
+
}),
|
|
53
|
+
Type.Object({
|
|
54
|
+
vb: Type.Boolean({ title: 'Boolean Value' }),
|
|
55
|
+
}),
|
|
56
|
+
Type.Object({
|
|
57
|
+
vd: Type.String({
|
|
58
|
+
title: 'Data Value.',
|
|
59
|
+
description:
|
|
60
|
+
'Octets in the Data Value are base64 encoded with the URL-safe alphabet as defined in Section 5 of [RFC4648], with padding omitted.',
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
]),
|
|
64
|
+
],
|
|
65
|
+
{
|
|
66
|
+
description:
|
|
67
|
+
'SenML schema for conversion results. This is limited to properties useful for the hello.nrfcloud.com/map application.',
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
export const SenML = Type.Array(Measurement, { minItems: 1 })
|
|
72
|
+
|
|
73
|
+
export type SenMLType = Static<typeof SenML>
|
|
74
|
+
export type MeasurementType = Static<typeof Measurement>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { hasValue } from './hasValue.js'
|
|
4
|
+
|
|
5
|
+
void describe('hasValue() should determine whether an object has a value', () => {
|
|
6
|
+
for (const [record, expected] of [
|
|
7
|
+
[{ bn: 14202, n: 0, v: 99, bt: 1699049685992 }, true],
|
|
8
|
+
[{ n: 1, v: 4.179 }, true],
|
|
9
|
+
[{ n: 2, v: 0 }, true],
|
|
10
|
+
[{ n: 3, v: 25.7 }, true],
|
|
11
|
+
[{ n: 4, v: null }, false],
|
|
12
|
+
[{ n: 5, v: null }, false],
|
|
13
|
+
]) {
|
|
14
|
+
void it(`should determine that ${JSON.stringify(
|
|
15
|
+
record,
|
|
16
|
+
)} has a value: ${JSON.stringify(expected)}`, () =>
|
|
17
|
+
assert.equal(hasValue(record), expected))
|
|
18
|
+
}
|
|
19
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const hasValue = (m: unknown): boolean => {
|
|
2
|
+
if (m === null) return false
|
|
3
|
+
if (typeof m !== 'object') return false
|
|
4
|
+
const v =
|
|
5
|
+
('bv' in m ? m.bv : undefined) ??
|
|
6
|
+
('v' in m ? m.v : undefined) ??
|
|
7
|
+
('vs' in m ? m.vs : undefined) ??
|
|
8
|
+
('vb' in m ? m.vb : undefined) ??
|
|
9
|
+
('vd' in m ? m.vd : undefined)
|
|
10
|
+
if (v === null || v === undefined) return false
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { it, describe } from 'node:test'
|
|
2
|
+
import type { SenMLType } from './SenMLSchema.js'
|
|
3
|
+
import type {
|
|
4
|
+
Geolocation_14201,
|
|
5
|
+
SeaWaterLevel_14230,
|
|
6
|
+
} from '../lwm2m/objects.js'
|
|
7
|
+
import { LwM2MObjectID } from '../lwm2m/LwM2MObjectID.js'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import type { LwM2MObjectInstance } from './senMLtoLwM2M.js'
|
|
10
|
+
import { lwm2mToSenML } from './lwm2mToSenML.js'
|
|
11
|
+
|
|
12
|
+
void describe('lwm2mToSenML()', () => {
|
|
13
|
+
void it('should convert LwM2M to SenML', () => {
|
|
14
|
+
const location: LwM2MObjectInstance<Geolocation_14201> = {
|
|
15
|
+
ObjectID: LwM2MObjectID.Geolocation_14201,
|
|
16
|
+
ObjectVersion: '1.0',
|
|
17
|
+
Resources: {
|
|
18
|
+
'0': 62.469414,
|
|
19
|
+
'1': 6.151946,
|
|
20
|
+
'6': 'Fixed',
|
|
21
|
+
'3': 1,
|
|
22
|
+
'99': new Date(1710147413003),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
const level1: LwM2MObjectInstance<SeaWaterLevel_14230> = {
|
|
26
|
+
ObjectID: LwM2MObjectID.SeaWaterLevel_14230,
|
|
27
|
+
ObjectVersion: '1.0',
|
|
28
|
+
Resources: {
|
|
29
|
+
'0': 84.3,
|
|
30
|
+
'1': 'AES',
|
|
31
|
+
'99': new Date(1710140400000),
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
const level2: LwM2MObjectInstance<SeaWaterLevel_14230> = {
|
|
35
|
+
ObjectID: LwM2MObjectID.SeaWaterLevel_14230,
|
|
36
|
+
ObjectVersion: '1.0',
|
|
37
|
+
Resources: {
|
|
38
|
+
'0': 140.4,
|
|
39
|
+
'1': 'AES',
|
|
40
|
+
'99': new Date(1710144000000),
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
const level3: LwM2MObjectInstance<SeaWaterLevel_14230> = {
|
|
44
|
+
ObjectID: LwM2MObjectID.SeaWaterLevel_14230,
|
|
45
|
+
ObjectVersion: '1.0',
|
|
46
|
+
ObjectInstanceID: 1,
|
|
47
|
+
Resources: {
|
|
48
|
+
'0': 140.7,
|
|
49
|
+
'1': 'AES',
|
|
50
|
+
'99': new Date(1710144001000),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
const lwm2m: Array<LwM2MObjectInstance<any>> = [
|
|
54
|
+
location,
|
|
55
|
+
level1,
|
|
56
|
+
level2,
|
|
57
|
+
level3,
|
|
58
|
+
]
|
|
59
|
+
const expected: SenMLType = [
|
|
60
|
+
{ bn: '/14201/0/', n: '0', v: 62.469414, bt: 1710147413003 },
|
|
61
|
+
{ n: '1', v: 6.151946 },
|
|
62
|
+
{ n: '3', v: 1 },
|
|
63
|
+
{ n: '6', vs: 'Fixed' },
|
|
64
|
+
{ bn: '/14230/0/', n: '0', v: 84.3, bt: 1710140400000 },
|
|
65
|
+
{ n: '1', vs: 'AES' },
|
|
66
|
+
{ bn: '/14230/0/', n: '0', v: 140.4, bt: 1710144000000 },
|
|
67
|
+
{ n: '1', vs: 'AES' },
|
|
68
|
+
{ bn: '/14230/1/', n: '0', v: 140.7, bt: 1710144001000 },
|
|
69
|
+
{ n: '1', vs: 'AES' },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(lwm2mToSenML(lwm2m), expected)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SenMLType } from './SenMLSchema'
|
|
2
|
+
import { instanceTs } from '../lwm2m/instanceTs.js'
|
|
3
|
+
import type { LwM2MObjectInstance, LwM2MResourceValue } from './senMLtoLwM2M'
|
|
4
|
+
import { timestampResources } from '../lwm2m/timestampResources.js'
|
|
5
|
+
import { definitions } from '../lwm2m/definitions.js'
|
|
6
|
+
import { ResourceType, type LWM2MObjectInfo } from '../lwm2m/LWM2MObjectInfo.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert LwM2M Object Instances to senML
|
|
10
|
+
*/
|
|
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 => {
|
|
20
|
+
const def = definitions[lwm2m.ObjectID]
|
|
21
|
+
const i = instanceTs(lwm2m)
|
|
22
|
+
const tsResourceId = timestampResources[lwm2m.ObjectID] as number // All registered objects must have a timestamp resource
|
|
23
|
+
const [first, ...rest] = Object.entries({
|
|
24
|
+
...lwm2m.Resources,
|
|
25
|
+
[tsResourceId]: undefined,
|
|
26
|
+
})
|
|
27
|
+
// Filter out undefined values (and timestamp resource)
|
|
28
|
+
.filter((r): r is [string, LwM2MResourceValue] => r[1] !== undefined)
|
|
29
|
+
|
|
30
|
+
if (first === undefined) return null
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
bn: `/${lwm2m.ObjectID}/${lwm2m.ObjectInstanceID ?? 0}/`,
|
|
34
|
+
n: first[0],
|
|
35
|
+
[toKey(def, parseInt(first[0], 10))]: first[1],
|
|
36
|
+
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
|
+
const toKey = (def: LWM2MObjectInfo, resourceId: number) => {
|
|
46
|
+
switch (def.Resources[resourceId]?.Type) {
|
|
47
|
+
case ResourceType.String:
|
|
48
|
+
return 'vs'
|
|
49
|
+
case ResourceType.Boolean:
|
|
50
|
+
return 'vb'
|
|
51
|
+
case ResourceType.Float:
|
|
52
|
+
case ResourceType.Integer:
|
|
53
|
+
case ResourceType.Time:
|
|
54
|
+
return 'v'
|
|
55
|
+
case ResourceType.Opaque:
|
|
56
|
+
return 'vd'
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { parseResourceId } from './parseResourceId.js'
|
|
4
|
+
|
|
5
|
+
void describe('parseResourceId()', () => {
|
|
6
|
+
void it('should parse an LwM2M resource ID', () =>
|
|
7
|
+
assert.deepEqual(parseResourceId('/14201/1/2/0'), {
|
|
8
|
+
ObjectID: 14201,
|
|
9
|
+
ObjectInstanceID: 1,
|
|
10
|
+
ResourceID: 2,
|
|
11
|
+
ResourceInstanceId: 0,
|
|
12
|
+
}))
|
|
13
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ResourceID = {
|
|
2
|
+
ObjectID: number
|
|
3
|
+
ObjectInstanceID: number
|
|
4
|
+
ResourceID: number
|
|
5
|
+
ResourceInstanceId: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const parseResourceId = (resourceId: string): ResourceID | null => {
|
|
9
|
+
if (!/^\/\d+\/\d+\/\d+\/\d+$/.test(resourceId)) return null
|
|
10
|
+
|
|
11
|
+
const [ObjectID, ObjectInstanceID, ResourceID, ResourceInstanceId] =
|
|
12
|
+
resourceId
|
|
13
|
+
.slice(1)
|
|
14
|
+
.split('/')
|
|
15
|
+
.map((s) => parseInt(s, 10)) as [number, number, number, number]
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ObjectID,
|
|
19
|
+
ObjectInstanceID,
|
|
20
|
+
ResourceID,
|
|
21
|
+
ResourceInstanceId,
|
|
22
|
+
}
|
|
23
|
+
}
|