diff --git a/.gitignore b/.gitignore index 210d272..e67380b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules/ npm-debug.log .swc/ cdk.out/ -dist/ \ No newline at end of file +dist/ +certificates/ \ No newline at end of file diff --git a/cdk/resources/WebsocketAPI.ts b/cdk/resources/WebsocketAPI.ts index f81868e..1c352f6 100644 --- a/cdk/resources/WebsocketAPI.ts +++ b/cdk/resources/WebsocketAPI.ts @@ -152,6 +152,7 @@ export class WebsocketAPI extends Construct { new IAM.PolicyStatement({ actions: [ 'iot:GetThingShadow', + 'iot:UpdateThingShadow', 'iot:ListThings', 'iot:DescribeThing', 'iot:SearchIndex', diff --git a/lambda/onMessage.ts b/lambda/onMessage.ts index 4b11812..ba89b39 100644 --- a/lambda/onMessage.ts +++ b/lambda/onMessage.ts @@ -15,6 +15,10 @@ import { IoTClient, SearchIndexCommand, } from '@aws-sdk/client-iot' +import { + UpdateThingShadowCommand, + type UpdateThingShadowCommandInput, +} from '@aws-sdk/client-iot-data-plane' import { ApiGatewayManagementApi } from '@aws-sdk/client-apigatewaymanagementapi' import { sendEvent } from './notifyClients.js' import type { LwM2MObjectInstance } from '@hello.nrfcloud.com/proto-lwm2m' @@ -26,13 +30,40 @@ const { TableName, websocketManagementAPIURL } = fromEnv({ websocketManagementAPIURL: 'WEBSOCKET_MANAGEMENT_API_URL', })(process.env) +const deviceControl = Type.Object({ + deviceId: Type.String({ minLength: 1 }), + code: Type.String({ minLength: 1 }), +}) + const message = Type.Object({ message: Type.Literal('sendmessage'), - data: Type.Object({ - deviceId: Type.String({ minLength: 1 }), - code: Type.String({ minLength: 1 }), - nrplusCtrl: Type.String({ minLength: 1 }), - }), + data: Type.Union([ + Type.Intersect([ + deviceControl, + Type.Object({ + nrplusCtrl: Type.String({ minLength: 1 }), + }), + ]), + Type.Intersect([ + deviceControl, + Type.Object({ + wirepasCtrl: Type.Object({ + nodes: Type.Record( + Type.String({ minLength: 1 }), + Type.Object({ + payload: Type.Object({ + led: Type.Object({ + r: Type.Boolean(), + g: Type.Boolean(), + b: Type.Boolean(), + }), + }), + }), + ), + }), + }), + ]), + ]), }) const validateMessage = validateWithTypeBox(message) @@ -169,7 +200,8 @@ export const handler = async ( statusCode: 400, } } else { - const { deviceId, code, nrplusCtrl } = maybeValidMessage.value.data + const msg = maybeValidMessage.value.data + const { deviceId, code } = msg const attributes = ( await iot.send(new DescribeThingCommand({ thingName: deviceId })) ).attributes @@ -183,14 +215,28 @@ export const handler = async ( body: `Code ${code} not valid for device ${deviceId}!`, } } - await iotData.send( - new PublishCommand({ - topic: `${deviceId}/nrplus-ctrl`, - payload: Buffer.from(nrplusCtrl, 'utf-8'), - qos: 1, - }), - ) - console.log(`>`, `${deviceId}/nrplus-ctrl`, nrplusCtrl) + if ('nrplusCtrl' in msg) { + await iotData.send( + new PublishCommand({ + topic: `${deviceId}/nrplus-ctrl`, + payload: Buffer.from(msg.nrplusCtrl, 'utf-8'), + qos: 1, + }), + ) + console.log(`>`, `${deviceId}/nrplus-ctrl`, msg.nrplusCtrl) + } + if ('wirepasCtrl' in msg) { + const update: UpdateThingShadowCommandInput = { + thingName: deviceId, + payload: JSON.stringify({ + state: { + desired: msg.wirepasCtrl, + }, + }), + } + await iotData.send(new UpdateThingShadowCommand(update)) + console.log(JSON.stringify({ update })) + } return { statusCode: 202, } diff --git a/package-lock.json b/package-lock.json index be3aced..62d5fd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@protobuf-ts/runtime": "2.9.3", "@sinclair/typebox": "0.32.14", "ajv": "8.12.0", + "aws-iot-device-sdk-v2": "1.19.0", "jsonata": "2.0.3", "lodash-es": "4.17.21", "mqtt": "5.3.5", + "p-throttle": "6.1.0", "protobufjs": "7.2.6" }, "devDependencies": { @@ -4477,6 +4479,48 @@ "npm": ">=9.0.0" } }, + "node_modules/@httptoolkit/websocket-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@httptoolkit/websocket-stream/-/websocket-stream-6.0.1.tgz", + "integrity": "sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==", + "dependencies": { + "@types/ws": "*", + "duplexify": "^3.5.1", + "inherits": "^2.0.1", + "isomorphic-ws": "^4.0.1", + "readable-stream": "^2.3.3", + "safe-buffer": "^5.1.2", + "ws": "*", + "xtend": "^4.0.0" + } + }, + "node_modules/@httptoolkit/websocket-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@httptoolkit/websocket-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@httptoolkit/websocket-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -6493,6 +6537,11 @@ "node": ">=0.10.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -6895,11 +6944,195 @@ "node": ">= 6" } }, + "node_modules/aws-crt": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/aws-crt/-/aws-crt-1.21.1.tgz", + "integrity": "sha512-pSLf1Xg5P2Owa+n/82hkEXV22q56kbxJOxX4FB5DvvHpTokOygz6nFj+/cS9dnv3tNZgOiUjTvsz0Xk1ldyi0w==", + "hasInstallScript": true, + "dependencies": { + "@aws-sdk/util-utf8-browser": "^3.109.0", + "@httptoolkit/websocket-stream": "^6.0.1", + "axios": "^1.6.0", + "buffer": "^6.0.3", + "crypto-js": "^4.2.0", + "mqtt": "^4.3.8", + "process": "^0.11.10" + } + }, + "node_modules/aws-crt/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/aws-crt/node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/aws-crt/node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "node_modules/aws-crt/node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/aws-crt/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-crt/node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, + "node_modules/aws-crt/node_modules/mqtt": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz", + "integrity": "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-crt/node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/aws-crt/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/aws-crt/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/aws-iot-device-sdk-v2": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/aws-iot-device-sdk-v2/-/aws-iot-device-sdk-v2-1.19.0.tgz", + "integrity": "sha512-zgX2W5uyVSGXyRMFy40ktacZadxqorIjcTD3i7HSc56rN57PFPHuqheHFL+KnrrPhbROOEanilEZf4z87/PC6Q==", + "dependencies": { + "@aws-sdk/util-utf8-browser": "^3.109.0", + "aws-crt": "^1.20.1" + } + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -7117,7 +7350,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7617,6 +7849,17 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -7644,8 +7887,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "2.0.0", @@ -7737,6 +7979,11 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -7794,6 +8041,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -7935,6 +8187,14 @@ "node": ">= 0.4" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7971,6 +8231,44 @@ "node": ">=8" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7993,7 +8291,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -8794,6 +9091,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8831,11 +9147,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -9437,7 +9765,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9775,12 +10102,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -9933,6 +10273,14 @@ "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10451,7 +10799,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10521,7 +10868,17 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, "engines": { "node": ">= 0.6" } @@ -10557,7 +10914,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10865,7 +11221,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -10932,7 +11287,22 @@ "node": ">=4" } }, - "node_modules/p-limit": { + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", @@ -10947,14 +11317,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { "node": ">=10" }, @@ -10962,6 +11329,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-throttle": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-6.1.0.tgz", + "integrity": "sha512-eQMdGTxk2+047La67wefUtt0tEHh7D+C8Jl7QXoFCuIiNYeQ9zWs2AZiJdIAs72rSXZ06t11me2bgalRNdy3SQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11014,7 +11392,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11231,6 +11608,11 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -11241,7 +11623,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11947,6 +12328,11 @@ "node": ">= 10.x" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12749,8 +13135,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.16.0", @@ -12772,6 +13157,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12784,8 +13177,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.3.4", @@ -12860,18 +13252,6 @@ "dependencies": { "buffer-crc32": "~0.2.3" } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index c7c8919..24cd0f3 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,11 @@ "@protobuf-ts/runtime": "2.9.3", "@sinclair/typebox": "0.32.14", "ajv": "8.12.0", + "aws-iot-device-sdk-v2": "1.19.0", "jsonata": "2.0.3", "lodash-es": "4.17.21", "mqtt": "5.3.5", + "p-throttle": "6.1.0", "protobufjs": "7.2.6" } } diff --git a/wirepas-5g-mesh-gateway/cloudToGateway.ts b/wirepas-5g-mesh-gateway/cloudToGateway.ts new file mode 100644 index 0000000..4188a41 --- /dev/null +++ b/wirepas-5g-mesh-gateway/cloudToGateway.ts @@ -0,0 +1,137 @@ +import { mqtt, io, iot, iotshadow } from 'aws-iot-device-sdk-v2' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import os from 'node:os' +import { + UpdateThingShadowCommand, + type IoTDataPlaneClient, +} from '@aws-sdk/client-iot-data-plane' + +io.enable_logging( + process.env.AWS_IOT_SDK_LOG_LEVEL === undefined + ? io.LogLevel.ERROR + : parseInt(process.env.AWS_IOT_SDK_LOG_LEVEL, 10), +) +const clientBootstrap = new io.ClientBootstrap() + +const connect = async ({ + clientCert, + privateKey, + deviceId, + mqttEndpoint, + log, +}: { + clientCert: string + privateKey: string + deviceId: string + mqttEndpoint: string + + log: { + debug: (...args: any[]) => void + error: (...args: any[]) => void + } +}) => + new Promise((resolve, reject) => { + const cfg = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder( + clientCert, + privateKey, + ) + cfg.with_clean_session(true) + cfg.with_client_id(deviceId) + cfg.with_endpoint(mqttEndpoint) + const client = new mqtt.MqttClient(clientBootstrap) + const connection = client.new_connection(cfg.build()) + connection.on('error', (err) => { + log.error(JSON.stringify(err)) + reject(err) + }) + connection.on('connect', () => { + log.debug(`${deviceId} connected`) + resolve(connection) + }) + connection.on('disconnect', () => { + log.debug(`${deviceId} disconnected`) + }) + connection.on('closed', () => { + log.debug(`${deviceId} closed`) + }) + connection.connect().catch(() => { + log.debug(`${deviceId} failed to connect.`) + }) + }) + +type Desired = { + nodes: Record< + string, + { payload: { led?: { r?: boolean; g?: boolean; b?: boolean } } } + > +} + +export const cloudToGateway = + ( + iotDataClient: IoTDataPlaneClient, + log: { + debug: (...args: any[]) => void + error: (...args: any[]) => void + }, + ) => + async ( + deviceId: string, + onDesired: (desired: Desired) => Promise, + ): Promise => { + const [privateKey, clientCert] = [ + readFileSync( + path.join(process.cwd(), 'certificates', `${deviceId}-private.pem.key`), + 'utf-8', + ), + [ + readFileSync( + path.join( + process.cwd(), + 'certificates', + `${deviceId}-certificate.pem.crt`, + ), + 'utf-8', + ), + readFileSync( + path.join(process.cwd(), 'certificates', `AmazonRootCA1.pem`), + 'utf-8', + ), + ].join(os.EOL), + ] + const connection = await connect({ + clientCert, + privateKey, + deviceId, + mqttEndpoint: 'iot.thingy.rocks', + log, + }) + const shadow = new iotshadow.IotShadowClient(connection) + + void shadow.subscribeToShadowDeltaUpdatedEvents( + { + thingName: deviceId, + }, + mqtt.QoS.AtLeastOnce, + async (err, response) => { + if (err !== undefined) { + log.error(err) + } + const desired = (response?.state ?? {}) as Desired + log.debug(JSON.stringify(desired)) + await onDesired(desired) + await iotDataClient.send( + new UpdateThingShadowCommand({ + thingName: deviceId, + payload: JSON.stringify({ + state: { + reported: desired, + }, + }), + }), + ) + }, + ) + + return connection + } diff --git a/wirepas-5g-mesh-gateway/gateway.ts b/wirepas-5g-mesh-gateway/gateway.ts index dd00f9a..164a08b 100644 --- a/wirepas-5g-mesh-gateway/gateway.ts +++ b/wirepas-5g-mesh-gateway/gateway.ts @@ -1,5 +1,6 @@ import { fromEnv } from '@nordicsemiconductor/from-env' import mqtt from 'mqtt' +import { mqtt as awsMqtt } from 'aws-iot-device-sdk-v2' import { debug, error, log } from './log.js' import { GenericMessage } from './protobuf/ts/generic_message.js' import { @@ -14,6 +15,10 @@ import { } from '@aws-sdk/client-iot' import { merge } from 'lodash-es' import { decodePayload } from './decodePayload.js' +import { cloudToGateway } from './cloudToGateway.js' +import { LED_COLOR, setLEDColor, wirepasPublish } from './publish.js' +import chalk from 'chalk' +import pThrottle from 'p-throttle' const { region, accessKeyId, secretAccessKey, gatewayEndpoint } = fromEnv({ region: 'GATEWAY_REGION', @@ -164,3 +169,47 @@ setInterval(async () => { nodes = {} }, stateFlushInterval * 1000) debug(`Flushing state every ${stateFlushInterval} seconds`) + +// Handle configuration changes +const C2G = chalk.blue.dim('C2G') +const sendToGateway = wirepasPublish({ + client, + debug: (...args) => debug(C2G, ...args), +}) +const gwThingConnections: Record = {} +const throttle = pThrottle({ + limit: 1, + interval: 250, +}) +const updateColor = throttle( + async (gwId: string, node: number, color: LED_COLOR, ledState: boolean) => + sendToGateway({ + gateway: gwId, + req: setLEDColor({ + node, + color, + ledState, + }), + }), +) +for (const gwId of Object.keys(existingGws)) { + gwThingConnections[gwId] = await cloudToGateway(iotDataClient, { + debug: (...args) => debug(C2G, ...args), + error: (...args) => error(C2G, ...args), + })(gwId, async (desired) => { + for (const [nodeId, { payload }] of Object.entries(desired.nodes)) { + const node = parseInt(nodeId, 10) + if ('led' in payload && payload.led !== undefined) { + const { r, g, b } = payload.led + const updates = [] + if (r !== undefined) + updates.push(updateColor(gwId, node, LED_COLOR.RED, r)) + if (g !== undefined) + updates.push(updateColor(gwId, node, LED_COLOR.GREEN, g)) + if (b !== undefined) + updates.push(updateColor(gwId, node, LED_COLOR.BLUE, b)) + await Promise.all(updates) + } + } + }) +} diff --git a/wirepas-5g-mesh-gateway/publish.ts b/wirepas-5g-mesh-gateway/publish.ts new file mode 100644 index 0000000..06baab4 --- /dev/null +++ b/wirepas-5g-mesh-gateway/publish.ts @@ -0,0 +1,126 @@ +import { randomBytes } from 'crypto' +import mqtt from 'mqtt' +import { GenericMessage } from '../wirepas-5g-mesh-gateway/protobuf/ts/generic_message.js' + +// https://github.com/wirepas/wm-sdk/tree/v1.4.0/source/example_apps/evaluation_app +enum ExampleAppMessages { + LED_STATE_SET = 129, + LED_STATE_GET = 130, +} + +// FIXME: change to 5 later +// For now keep the LED ID as “0” (current boards do not manage this but for the demo it will be “5”. +const LED_ID = 0 + +export enum LED_COLOR { + RED = 0, + BLUE = 1, + GREEN = 2, + ALL = 255, +} + +const sendPacket = (node: number, payload: Buffer): GenericMessage => ({ + wirepas: { + sendPacketReq: { + qos: 2, // send the downlink messages (i.e from backend to nodes) with MQTT QoS 2 so that they are delivered only once to the gateway. + sourceEndpoint: 1, + destinationAddress: node, + destinationEndpoint: 1, + payload, + header: { + reqId: BigInt('0x' + randomBytes(8).toString('hex')), + }, + }, + }, +}) + +/** + * Controls an LED + * + * On this version, we can drive red, blue, green and all three user LED (respectively with color code 00, 01, 02 and FF) and its state (on: 01, off: 00). + * Therefore, the full command is 81 [color] [state] (3 bytes). + * + * @see https://github.com/wirepas/wm-sdk/tree/v1.4.0/source/example_apps/evaluation_app#led-state-set-message + */ +export const setLEDColor = ({ + node, + color, + ledState, +}: { + node: number + color: LED_COLOR + ledState: boolean +}): GenericMessage => + sendPacket( + node, + Buffer.from([ + // LED state set message + ExampleAppMessages.LED_STATE_SET, + // LED identifier (Decimal value 0 is the first user available LED on the node) + color, + // LED state (Decimal value 0 => switch off, 1 => switch on) + ledState ? 1 : 0, + ]), + ) + +/** + * Query LED state + * + * @see https://github.com/wirepas/wm-sdk/tree/v1.4.0/source/example_apps/evaluation_app#led-state-get-request-message + */ +export const getLedState = ({ node }: { node: number }): GenericMessage => + sendPacket( + node, + Buffer.from([ + // LED state set message + ExampleAppMessages.LED_STATE_GET, + // LED identifier (Decimal value 0 is the first user available LED on the node) + LED_ID, + ]), + ) + +/** + * Publish a message to the Wirepas 5G Mesh Gateway + * @see https://github.com/wirepas/backend-apis/tree/v1.4.0/gateway_to_backend#data-module + + */ +export const wirepasPublish = + ({ + client, + debug, + }: { + client: mqtt.MqttClient + debug: (...args: any[]) => void + }) => + async ({ + gateway, + req, + }: { + gateway: string + req: GenericMessage + }): Promise => { + const topic = `gw-request/send_data/${gateway}/sink1` + + debug('Publishing to', topic) + debug( + JSON.stringify(req, (key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), + ) + debug( + Buffer.from(Buffer.from(GenericMessage.toBinary(req))).toString('hex'), + ) + + await new Promise((resolve, reject) => { + client.publish( + topic, + Buffer.from(GenericMessage.toBinary(req)), + (err, res) => { + if (err !== undefined) return reject(err) + return resolve(res) + }, + ) + }) + + client.end() + }