diff --git a/.github/workflows/ci-full.yaml b/.github/workflows/ci-full.yaml index 34dd79cfa..854c052c1 100644 --- a/.github/workflows/ci-full.yaml +++ b/.github/workflows/ci-full.yaml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - node-version: [16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - name: Checkout @@ -19,9 +19,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" - - name: npm 7 - # npm workspaces requires npm v7 or higher - run: npm i -g npm@7 --registry=https://registry.npmjs.org - name: Install run: npm ci @@ -41,14 +38,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 14 + - name: Use Node.js 16 uses: actions/setup-node@v1 with: - node-version: 14 - - - name: npm 7 - # npm workspaces requires npm v7 or higher - run: npm i -g npm@7 --registry=https://registry.npmjs.org + node-version: 16 - name: Install run: npm ci @@ -61,10 +54,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: install node v14 + - name: install node v16 uses: actions/setup-node@v1 with: - node-version: 14 + node-version: 16 - name: verify packages version consistency accross sub-modules run: npm run check:versions diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b886d4343..209bb9480 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - name: Checkout @@ -19,9 +19,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" - - name: npm 7 - # npm workspaces requires npm v7 or higher - run: npm i -g npm@7 --registry=https://registry.npmjs.org - name: Install run: npm ci @@ -41,14 +38,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 14 + - name: Use Node.js 16 uses: actions/setup-node@v1 with: - node-version: 14 - - - name: npm 7 - # npm workspaces requires npm v7 or higher - run: npm i -g npm@7 --registry=https://registry.npmjs.org + node-version: 16 - name: Install run: npm ci @@ -61,10 +54,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: install node v14 + - name: install node v16 uses: actions/setup-node@v1 with: - node-version: 14 + node-version: 16 - name: verify packages version consistency accross sub-modules run: npm run check:versions diff --git a/README.md b/README.md index cb45820f9..21a73413c 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ cs.addCodec(new MyCodec("application/myType")); ### To use with Node.js > **Warning**: We no longer actively support Node.js version 14 and lower. -> Node.js version 20 is not yet supported (https://github.com/eclipse-thingweb/node-wot/issues/1004) - [Node.js](https://nodejs.org/) version 14+ - [npm](https://www.npmjs.com/) version 7+ diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..4c674a82b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true +# When modifying this file, please validate using +# curl -X POST --data-binary @codecov.yml https://codecov.io/validate diff --git a/examples/scripts/countdown.js b/examples/scripts/countdown.js index c0b9971f7..3c4f425bd 100644 --- a/examples/scripts/countdown.js +++ b/examples/scripts/countdown.js @@ -80,7 +80,7 @@ WoT.produce({ const listToDelete = []; for (const id of countdowns.keys()) { const as = countdowns.get(id); - if (as.output !== undefined) { + if (as !== undefined && as.output !== undefined) { const prev = as.output; as.output--; console.log("\t" + id + ", from " + prev + " to " + as.output); @@ -125,7 +125,7 @@ WoT.produce({ }; const ii = resp; console.log("init countdown value = " + JSON.stringify(resp)); - countdowns.set(resp.href, resp); + countdowns.set(resp.href !== undefined ? resp.href : "", resp); return ii; }); thing.setActionHandler("stopCountdown", async (params, options) => { @@ -133,10 +133,14 @@ WoT.produce({ const value = await params.value(); if (typeof value === "string" && countdowns.has(value)) { const as = countdowns.get(value); - as.output = 0; - as.status = Status.completed; - console.log("Countdown stopped for href: " + value); - return undefined; + if (as !== undefined) { + as.output = 0; + as.status = Status.completed; + console.log("Countdown stopped for href: " + value); + return null; + } else { + throw Error("Countdown value is undefined for href, " + value); + } } else { throw Error("Input provided for stopCountdown is no string or invalid href, " + value); } diff --git a/examples/scripts/counter-client.js b/examples/scripts/counter-client.js index a8cf994cf..d98973408 100644 --- a/examples/scripts/counter-client.js +++ b/examples/scripts/counter-client.js @@ -13,6 +13,19 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ +function getFormIndexForDecrementWithCoAP(thing) { + var _a; + const forms = (_a = thing.getThingDescription().actions) === null || _a === void 0 ? void 0 : _a.decrement.forms; + if (forms !== undefined) { + for (let i = 0; i < forms.length; i++) { + if (/^coaps?:\/\/.*/.test(forms[i].href)) { + return i; + } + } + } + // return formIndex: 0 if no CoAP target IRI found + return 0; +} WoTHelpers.fetch("coap://localhost:5683/counter") .then(async (td) => { // using await for serial execution (note 'async' in then() of fetch()) @@ -45,13 +58,3 @@ WoTHelpers.fetch("coap://localhost:5683/counter") .catch((err) => { console.error("Fetch error:", err); }); -function getFormIndexForDecrementWithCoAP(thing) { - const forms = thing.getThingDescription().actions.decrement.forms; - for (let i = 0; i < forms.length; i++) { - if (/^coaps?:\/\/.*/.test(forms[i].href)) { - return i; - } - } - // return formIndex: 0 if no CoAP target IRI found - return 0; -} diff --git a/examples/scripts/counter.js b/examples/scripts/counter.js index ea7828f39..19939def7 100644 --- a/examples/scripts/counter.js +++ b/examples/scripts/counter.js @@ -204,7 +204,7 @@ WoT.produce({ let fill = "black"; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("fill" in options.uriVariables) { + if (options.uriVariables && "fill" in options.uriVariables) { const uriVariables = options.uriVariables; fill = uriVariables.fill; } @@ -229,7 +229,7 @@ WoT.produce({ let step = 1; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("step" in options.uriVariables) { + if (options.uriVariables && "step" in options.uriVariables) { const uriVariables = options.uriVariables; step = uriVariables.step; } @@ -246,7 +246,7 @@ WoT.produce({ let step = 1; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("step" in options.uriVariables) { + if (options.uriVariables && "step" in options.uriVariables) { const uriVariables = options.uriVariables; step = uriVariables.step; } diff --git a/examples/scripts/smart-coffee-machine-client.js b/examples/scripts/smart-coffee-machine-client.js index 8427eb2a9..9dba78129 100644 --- a/examples/scripts/smart-coffee-machine-client.js +++ b/examples/scripts/smart-coffee-machine-client.js @@ -15,6 +15,14 @@ // This is an example of Web of Things consumer ("client" mode) Thing script. // It considers a fictional smart coffee machine in order to demonstrate the capabilities of Web of Things. // An accompanying tutorial is available at http://www.thingweb.io/smart-coffee-machine.html. + +// Print data and an accompanying message in a distinguishable way +function log(msg, data) { + console.info("======================"); + console.info(msg); + console.dir(data); + console.info("======================"); +} WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) => { try { const thing = await WoT.consume(td); @@ -40,7 +48,7 @@ WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) = const makeCoffee = await thing.invokeAction("makeDrink", undefined, { uriVariables: { drinkId: "latte", size: "l", quantity: 3 }, }); - const makeCoffeep = await makeCoffee.value(); + const makeCoffeep = await (makeCoffee === null || makeCoffee === void 0 ? void 0 : makeCoffee.value()); if (makeCoffeep.result) { log("Enjoy your drink!", makeCoffeep); } else { @@ -57,7 +65,9 @@ WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) = time: "10:00", mode: "everyday", }); - const scheduledTaskp = await scheduledTask.value(); + const scheduledTaskp = await (scheduledTask === null || scheduledTask === void 0 + ? void 0 + : scheduledTask.value()); log(scheduledTaskp.message, scheduledTaskp); // See how it has been added to the schedules property const schedules = await (await thing.readProperty("schedules")).value(); @@ -74,10 +84,3 @@ WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) = console.error("Script error:", err); } }); -// Print data and an accompanying message in a distinguishable way -function log(msg, data) { - console.info("======================"); - console.info(msg); - console.dir(data); - console.info("======================"); -} diff --git a/examples/security/oauth/consumer.js b/examples/security/oauth/consumer.js index b6a6fd436..5726a0186 100644 --- a/examples/security/oauth/consumer.js +++ b/examples/security/oauth/consumer.js @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2018 - 2020 Contributors to the Eclipse Foundation + * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -15,7 +15,8 @@ WoTHelpers.fetch("https://localhost:8080/oauth").then((td) => { WoT.consume(td).then(async (thing) => { try { - const result = await (await thing.invokeAction("sayOk")).value(); + const resp = await thing.invokeAction("sayOk"); + const result = resp === null || resp === void 0 ? void 0 : resp.value(); console.log("oAuth token was", result); } catch (error) { console.log("It seems that I couldn't access the resource"); diff --git a/examples/testthing/testclient.js b/examples/testthing/testclient.js index 8c75c1140..c0815cd3c 100644 --- a/examples/testthing/testclient.js +++ b/examples/testthing/testclient.js @@ -25,7 +25,7 @@ async function testPropertyRead(thing, name) { const value = await res.value(); console.info("PASS " + name + " READ:", value); } catch (err) { - console.error("FAIL " + name + " READ:", err.message); + console.error("FAIL " + name + " READ:", JSON.stringify(err)); } } async function testPropertyWrite(thing, name, value, shouldFail) { @@ -35,8 +35,8 @@ async function testPropertyWrite(thing, name, value, shouldFail) { if (!shouldFail) console.info("PASS " + name + " WRITE (" + displayValue + ")"); else console.error("FAIL " + name + " WRITE: (" + displayValue + ")"); } catch (err) { - if (!shouldFail) console.error("FAIL " + name + " WRITE (" + displayValue + "):", err.message); - else console.info("PASS " + name + " WRITE (" + displayValue + "):", err.message); + if (!shouldFail) console.error("FAIL " + name + " WRITE (" + displayValue + "):", JSON.stringify(err)); + else console.info("PASS " + name + " WRITE (" + displayValue + "):", JSON.stringify(err)); } } WoTHelpers.fetch("http://localhost:8080/testthing") diff --git a/package-lock.json b/package-lock.json index 81561862c..cb7eec60f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7599,7 +7599,8 @@ "node_modules/nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true }, "node_modules/nanobench": { "version": "2.1.1", @@ -7860,17 +7861,34 @@ } }, "node_modules/node-mbus": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/node-mbus/-/node-mbus-1.2.2.tgz", - "integrity": "sha512-Acc2Y04U5DHHQO3Bc05chUy0+vcsy07PeTU68HWV2KG8ckS44RNbXP1lfcPvb9ulwy0178pZpqWjO8f7Zp/WTg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-mbus/-/node-mbus-2.1.0.tgz", + "integrity": "sha512-2yalJliG3e6r6lcxU5Hk/YeNmtebaPiG2FhT2kVAA56lESr/aUUWAyyZJSuzn8AwRkAEI3o/DCQq7dLC3QiMEw==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", - "nan": "~2.14.2", - "xml2js": "^0.4.23" + "nan": "^2.17.0", + "xml2js": "^0.6.2" }, "engines": { - "node": ">=6" + "node": ">=12.0.0" + } + }, + "node_modules/node-mbus/node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/node-mbus/node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, "node_modules/node-netconf": { @@ -12785,7 +12803,7 @@ "dependencies": { "@node-wot/core": "0.8.8", "@node-wot/td-tools": "0.8.8", - "node-mbus": "^1.2.2", + "node-mbus": "^2.1.0", "wot-typescript-definitions": "0.8.0-SNAPSHOT.26" }, "devDependencies": { diff --git a/packages/binding-mbus/package.json b/packages/binding-mbus/package.json index 95ddf261e..ed731b9f3 100644 --- a/packages/binding-mbus/package.json +++ b/packages/binding-mbus/package.json @@ -39,7 +39,7 @@ "dependencies": { "@node-wot/core": "0.8.8", "@node-wot/td-tools": "0.8.8", - "node-mbus": "^1.2.2", + "node-mbus": "^2.1.0", "wot-typescript-definitions": "0.8.0-SNAPSHOT.26" }, "scripts": { diff --git a/packages/binding-modbus/src/modbus-client-factory.ts b/packages/binding-modbus/src/modbus-client-factory.ts index 059b9d406..29173f710 100644 --- a/packages/binding-modbus/src/modbus-client-factory.ts +++ b/packages/binding-modbus/src/modbus-client-factory.ts @@ -20,12 +20,13 @@ const warn = createWarnLogger("binding-modbus", "modbus-client-factory"); export default class ModbusClientFactory implements ProtocolClientFactory { public readonly scheme: string = "modbus+tcp"; - private singleton: ModbusClient; + private singleton?: ModbusClient; public getClient(): ProtocolClient { debug(`Get client for '${this.scheme}'`); this.init(); - return this.singleton; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- singleton is initialized in init() + return this.singleton!; } public init(): boolean { diff --git a/packages/binding-modbus/src/modbus-client.ts b/packages/binding-modbus/src/modbus-client.ts index a2a66df3a..c191ad661 100644 --- a/packages/binding-modbus/src/modbus-client.ts +++ b/packages/binding-modbus/src/modbus-client.ts @@ -20,7 +20,7 @@ import { ModbusForm, ModbusFunction } from "./modbus"; import { ProtocolClient, Content, DefaultContent, createDebugLogger, Endianness } from "@node-wot/core"; import { SecurityScheme } from "@node-wot/td-tools"; import { modbusFunctionToEntity } from "./utils"; -import { ModbusConnection, PropertyOperation } from "./modbus-connection"; +import { ModbusConnection, ModbusFormWithDefaults, PropertyOperation } from "./modbus-connection"; import { Readable } from "stream"; import { Subscription } from "rxjs/Subscription"; @@ -51,7 +51,7 @@ class ModbusSubscription { next(result); } catch (e) { if (error) { - error(e); + error(e instanceof Error ? e : new Error(JSON.stringify(e))); } clearInterval(this.interval); } @@ -94,7 +94,12 @@ export default class ModbusClient implements ProtocolClient { form = this.validateAndFillDefaultForm(form, 0); const id = `${form.href}/${form["modbus:unitID"]}#${form["modbus:function"]}?${form["modbus:address"]}&${form["modbus:quantity"]}`; - this._subscriptions.get(id).unsubscribe(); + const subscription = this._subscriptions.get(id); + if (!subscription) { + throw new Error("No subscription for " + id + " found"); + } + subscription.unsubscribe(); + this._subscriptions.delete(id); return Promise.resolve(); @@ -148,15 +153,15 @@ export default class ModbusClient implements ProtocolClient { if (content) { body = await content.toBuffer(); } - form = this.validateAndFillDefaultForm(form, body?.byteLength); + const formValidated = this.validateAndFillDefaultForm(form, body?.byteLength); - const endianness = this.validateEndianness(form); + const endianness = this.validateEndianness(formValidated); const host = parsed.hostname; const hostAndPort = host + ":" + port; if (body) { - this.validateBufferLength(form, body); + this.validateBufferLength(formValidated, body); } // find or create connection @@ -164,16 +169,16 @@ export default class ModbusClient implements ProtocolClient { if (!connection) { debug(`Creating new ModbusConnection for ${hostAndPort}`); - this._connections.set( - hostAndPort, - new ModbusConnection(host, port, { connectionTimeout: form["modbus:timeout"] || DEFAULT_TIMEOUT }) - ); - connection = this._connections.get(hostAndPort); + + connection = new ModbusConnection(host, port, { + connectionTimeout: form["modbus:timeout"] || DEFAULT_TIMEOUT, + }); + this._connections.set(hostAndPort, connection); } else { debug(`Reusing ModbusConnection for ${hostAndPort}`); } // create operation - const operation = new PropertyOperation(form, endianness, body); + const operation = new PropertyOperation(formValidated, endianness, body); // enqueue the operation at the connection connection.enqueue(operation); @@ -206,10 +211,14 @@ export default class ModbusClient implements ProtocolClient { input["modbus:unitID"] = parseInt(pathComp[1], 10) || input["modbus:unitID"]; input["modbus:address"] = parseInt(pathComp[2], 10) || input["modbus:address"]; - input["modbus:quantity"] = parseInt(query.get("quantity"), 10) || input["modbus:quantity"]; + + const queryQuantity = query.get("quantity"); + if (queryQuantity) { + input["modbus:quantity"] = parseInt(queryQuantity, 10); + } } - private validateBufferLength(form: ModbusForm, buffer: Buffer) { + private validateBufferLength(form: ModbusFormWithDefaults, buffer: Buffer) { const mpy = form["modbus:entity"] === "InputRegister" || form["modbus:entity"] === "HoldingRegister" ? 2 : 1; const quantity = form["modbus:quantity"]; if (buffer && buffer.length !== mpy * quantity) { @@ -223,7 +232,7 @@ export default class ModbusClient implements ProtocolClient { } } - private validateAndFillDefaultForm(form: ModbusForm, contentLength = 0): ModbusForm { + private validateAndFillDefaultForm(form: ModbusForm, contentLength = 0): ModbusFormWithDefaults { const mode = contentLength > 0 ? "w" : "r"; // Use form values if provided, otherwise use form values (we are more merciful then the spec for retro-compatibility) @@ -243,7 +252,7 @@ export default class ModbusClient implements ProtocolClient { } // Check if the function is a valid modbus function code - if (!Object.keys(ModbusFunction).includes(result["modbus:function"].toString())) { + if (!Object.keys(ModbusFunction).includes(form["modbus:function"].toString())) { throw new Error("Undefined function number or name: " + form["modbus:function"]); } } @@ -296,6 +305,6 @@ export default class ModbusClient implements ProtocolClient { result["modbus:pollingTime"] = form["modbus:pollingTime"] ? form["modbus:pollingTime"] : DEFAULT_POLLING; result["modbus:timeout"] = form["modbus:timeout"] ? form["modbus:timeout"] : DEFAULT_TIMEOUT; - return result; + return result as ModbusFormWithDefaults; } } diff --git a/packages/binding-modbus/src/modbus-connection.ts b/packages/binding-modbus/src/modbus-connection.ts index bc79da3e8..9598ae611 100644 --- a/packages/binding-modbus/src/modbus-connection.ts +++ b/packages/binding-modbus/src/modbus-connection.ts @@ -15,13 +15,14 @@ import ModbusRTU from "modbus-serial"; import { ReadCoilResult, ReadRegisterResult } from "modbus-serial/ModbusRTU"; import { ModbusEntity, ModbusFunction, ModbusForm } from "./modbus"; -import { Content, createLoggers, Endianness } from "@node-wot/core"; +import { Content, ContentSerdes, createLoggers, Endianness } from "@node-wot/core"; import { Readable } from "stream"; import { inspect } from "util"; const { debug, warn, error } = createLoggers("binding-modbus", "modbus-connection"); const configDefaults = { + connectionTimeout: 1000, operationTimeout: 2000, connectionRetryTime: 10000, maxRetries: 5, @@ -100,7 +101,7 @@ class ModbusTransaction { } catch (err) { warn(`Read operation failed on ${this.base}, len: ${this.quantity}, ${err}`); // inform all operations and the invoker - this.operations.forEach((op) => op.failed(err)); + this.operations.forEach((op) => op.failed(err instanceof Error ? err : new Error(JSON.stringify(err)))); throw err; } } else { @@ -111,13 +112,27 @@ class ModbusTransaction { } catch (err) { warn(`Write operation failed on ${this.base}, len: ${this.quantity}, ${err}`); // inform all operations and the invoker - this.operations.forEach((op) => op.failed(err)); + this.operations.forEach((op) => op.failed(err instanceof Error ? err : new Error(JSON.stringify(err)))); throw err; } } } } +export type ModbusFormWithDefaults = ModbusForm & + Required< + Pick< + ModbusForm, + | "modbus:function" + | "modbus:entity" + | "modbus:unitID" + | "modbus:address" + | "modbus:quantity" + | "modbus:timeout" + | "modbus:pollingTime" + > + >; + /** * ModbusConnection represents a client connected to a specific host and port */ @@ -127,14 +142,14 @@ export class ModbusConnection { client: ModbusRTU; connecting: boolean; connected: boolean; - timer: NodeJS.Timer; // connection idle timer - currentTransaction: ModbusTransaction; // transaction currently in progress or null + timer: NodeJS.Timer | null; // connection idle timer + currentTransaction: ModbusTransaction | null; // transaction currently in progress or null queue: Array; // queue of further transactions config: { - connectionTimeout?: number; - operationTimeout?: number; - connectionRetryTime?: number; - maxRetries?: number; + connectionTimeout: number; + operationTimeout: number; + connectionRetryTime: number; + maxRetries: number; }; constructor( @@ -150,6 +165,7 @@ export class ModbusConnection { this.host = host; this.port = port; this.client = new ModbusRTU(); // new ModbusClient(); + this.connected = false; this.connecting = false; this.timer = null; this.currentTransaction = null; @@ -182,8 +198,8 @@ export class ModbusConnection { if (op.base === t.base + t.quantity) { // append t.quantity += op.quantity; - - if (t.content) { + // write operation + if (t.content && op.content) { t.content = Buffer.concat([t.content, op.content]); } @@ -196,7 +212,8 @@ export class ModbusConnection { t.base -= op.quantity; t.quantity += op.quantity; - if (t.content) { + // write operation + if (t.content && op.content) { t.content = Buffer.concat([op.content, t.content]); } @@ -268,13 +285,14 @@ export class ModbusConnection { // inform all the operations that the connection cannot be recovered this.queue.forEach((transaction) => { transaction.operations.forEach((op) => { - op.failed(error); + op.failed(error instanceof Error ? error : new Error(JSON.stringify(error))); }); }); } } else if (this.client.isOpen && this.currentTransaction == null && this.queue.length > 0) { // take next transaction from queue and execute - this.currentTransaction = this.queue.shift(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- queue.length > 0 + this.currentTransaction = this.queue.shift()!; try { await this.currentTransaction.execute(); this.currentTransaction = null; @@ -324,6 +342,10 @@ export class ModbusConnection { clearTimeout(this.timer); } + if (!transaction.content) { + throw new Error("Invoked write transaction without content"); + } + this.timer = global.setTimeout(() => this.modbusstop(), this.config.operationTimeout); const modFunc: ModbusFunction = transaction.function; @@ -399,7 +421,7 @@ export class ModbusConnection { error(`Cannot close session. ${err}`); } }); - clearInterval(this.timer); + this.timer && clearInterval(this.timer); this.timer = null; } } @@ -415,19 +437,19 @@ export class PropertyOperation { function: ModbusFunction; content?: Buffer; endianness: Endianness; - transaction: ModbusTransaction; // transaction used to execute this operation + transaction: ModbusTransaction | null; // transaction used to execute this operation contentType: string; - resolve: (value?: Content | PromiseLike) => void; - reject: (reason?: Error) => void; + resolve?: (value?: Content | PromiseLike) => void; + reject?: (reason?: Error) => void; - constructor(form: ModbusForm, endianness: Endianness, content?: Buffer) { + constructor(form: ModbusFormWithDefaults, endianness: Endianness, content?: Buffer) { this.unitId = form["modbus:unitID"]; this.registerType = form["modbus:entity"]; this.base = form["modbus:address"]; this.quantity = form["modbus:quantity"]; this.function = form["modbus:function"] as ModbusFunction; this.endianness = endianness; - this.contentType = form.contentType; + this.contentType = form.contentType ?? ContentSerdes.DEFAULT; this.content = content; this.transaction = null; } @@ -436,7 +458,7 @@ export class PropertyOperation { * Trigger execution of this operation. * */ - async execute(): Promise> { + async execute(): Promise<(Content | PromiseLike) | undefined> { return new Promise( (resolve: (value?: Content | PromiseLike) => void, reject: (reason?: Error) => void) => { this.resolve = resolve; @@ -461,12 +483,21 @@ export class PropertyOperation { done(base?: number, buffer?: Buffer): void { debug("Operation done"); + if (!this.resolve || !this.reject) { + throw new Error("Function 'done' was invoked before executing the Modbus operation"); + } + if (base === null || base === undefined) { // resolve write operation this.resolve(); return; } + if (buffer === null || buffer === undefined) { + this.reject(new Error("Write operation finished without buffer")); + return; + } + // extract the proper part from the result and resolve promise const address = this.base - base; let resp: Content; @@ -490,6 +521,9 @@ export class PropertyOperation { */ failed(reason: Error): void { warn(`Operation failed: ${reason}`); + if (!this.reject) { + throw new Error("Function 'failed' was invoked before executing the Modbus operation"); + } // reject the Promise given to the invoking script this.reject(reason); } diff --git a/packages/binding-modbus/test/modbus-connection-test.ts b/packages/binding-modbus/test/modbus-connection-test.ts index 2e5126328..4b1cc2522 100644 --- a/packages/binding-modbus/test/modbus-connection-test.ts +++ b/packages/binding-modbus/test/modbus-connection-test.ts @@ -14,10 +14,10 @@ ********************************************************************************/ import { should } from "chai"; import * as chai from "chai"; -import { ModbusForm } from "../src/modbus"; +import { ModbusFunction } from "../src/modbus"; import ModbusServer from "./test-modbus-server"; import chaiAsPromised from "chai-as-promised"; -import { ModbusConnection, PropertyOperation } from "../src/modbus-connection"; +import { ModbusConnection, ModbusFormWithDefaults, PropertyOperation } from "../src/modbus-connection"; import { Endianness } from "@node-wot/core"; // should must be called to augment all variables @@ -63,12 +63,15 @@ describe("Modbus connection", () => { describe("Operation", () => { it("should fail for unknown host", async () => { - const form: ModbusForm = { + const form: ModbusFormWithDefaults = { href: "modbus://127.0.0.2:8502", "modbus:function": 15, "modbus:address": 0, "modbus:quantity": 1, "modbus:unitID": 1, + "modbus:entity": "HoldingRegister", + "modbus:timeout": 1000, + "modbus:pollingTime": 1000, }; const connection = new ModbusConnection("127.0.0.2", 8503, { connectionTimeout: 200, @@ -83,12 +86,15 @@ describe("Modbus connection", () => { }).timeout(5000); it("should throw with timeout", async () => { - const form: ModbusForm = { + const form: ModbusFormWithDefaults = { href: "modbus://127.0.0.1:8502", + "modbus:function": ModbusFunction.readCoil, "modbus:entity": "Coil", "modbus:address": 4444, "modbus:quantity": 1, "modbus:unitID": 1, + "modbus:timeout": 1000, + "modbus:pollingTime": 1000, }; const connection = new ModbusConnection("127.0.0.1", 8502, { connectionTimeout: 100, diff --git a/packages/binding-modbus/test/test-modbus-server.ts b/packages/binding-modbus/test/test-modbus-server.ts index 6ac7e3b4e..f0eeba4dd 100644 --- a/packages/binding-modbus/test/test-modbus-server.ts +++ b/packages/binding-modbus/test/test-modbus-server.ts @@ -83,7 +83,7 @@ export default class ModbusServer { error(err.toString()); }); this.serverTCP.on("error", (err) => { - debug(err.toString()); + debug(err?.toString()); }); this.serverTCP.on("initialized", resolve); diff --git a/packages/binding-modbus/tsconfig.json b/packages/binding-modbus/tsconfig.json index e9fa7c062..df9cd1d90 100644 --- a/packages/binding-modbus/tsconfig.json +++ b/packages/binding-modbus/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "strict": true }, "include": ["src/**/*"], "references": [{ "path": "../td-tools" }, { "path": "../core" }] diff --git a/packages/binding-mqtt/src/mqtt-broker-server.ts b/packages/binding-mqtt/src/mqtt-broker-server.ts index c7a6d0376..7522ecc6f 100644 --- a/packages/binding-mqtt/src/mqtt-broker-server.ts +++ b/packages/binding-mqtt/src/mqtt-broker-server.ts @@ -41,6 +41,14 @@ import { Readable } from "stream"; const { info, debug, error, warn } = createLoggers("binding-mqtt", "mqtt-broker-server"); export default class MqttBrokerServer implements ProtocolServer { + private static brokerIsInitialized(broker?: mqtt.MqttClient): asserts broker is mqtt.MqttClient { + if (broker === undefined) { + throw new Error( + `Broker not initialized. You need to start the ${MqttBrokerServer.name} before you can expose things.` + ); + } + } + readonly scheme: string = "mqtt"; private readonly ACTION_SEGMENT_LENGTH = 3; @@ -51,30 +59,33 @@ export default class MqttBrokerServer implements ProtocolServer { private readonly INTERACTION_NAME_SEGMENT_INDEX = 2; private readonly INTERACTION_EXT_SEGMENT_INDEX = 3; + private readonly defaults: MqttBrokerServerConfig = { uri: "mqtt://localhost:1883" }; + private port = -1; - private address: string = undefined; + private address?: string = undefined; - private brokerURI: string = undefined; + private brokerURI: string; private readonly things: Map = new Map(); private readonly config: MqttBrokerServerConfig; - private broker: mqtt.MqttClient; + private broker?: mqtt.MqttClient; - private hostedServer: Aedes; - private hostedBroker: net.Server; + private hostedServer?: Aedes; + private hostedBroker?: net.Server; constructor(config: MqttBrokerServerConfig) { - this.config = config ?? { uri: "mqtt://localhost:1883" }; + this.config = config ?? this.defaults; + this.config.uri = this.config.uri ?? this.defaults.uri; - if (config.uri !== undefined) { - // if there is a MQTT protocol indicator missing, add this - if (config.uri.indexOf("://") === -1) { - config.uri = this.scheme + "://" + config.uri; - } - this.brokerURI = config.uri; + // if there is a MQTT protocol indicator missing, add this + if (config.uri.indexOf("://") === -1) { + config.uri = this.scheme + "://" + config.uri; } + + this.brokerURI = config.uri; + if (config.selfHost) { this.hostedServer = Server({}); let server; @@ -86,7 +97,7 @@ export default class MqttBrokerServer implements ProtocolServer { const parsed = new url.URL(this.brokerURI); const port = parseInt(parsed.port); this.port = port > 0 ? port : 1883; - this.hostedBroker = server.listen(port); + this.hostedBroker = server.listen(port, parsed.hostname); this.hostedServer.authenticate = this.selfHostAuthentication.bind(this); } } @@ -130,6 +141,7 @@ export default class MqttBrokerServer implements ProtocolServer { } private exposeProperty(name: string, propertyName: string, thing: ExposedThing) { + MqttBrokerServer.brokerIsInitialized(this.broker); const topic = encodeURIComponent(name) + "/properties/" + encodeURIComponent(propertyName); const property = thing.properties[propertyName]; @@ -143,6 +155,12 @@ export default class MqttBrokerServer implements ProtocolServer { const observeListener = async (content: Content) => { debug(`MqttBrokerServer at ${this.brokerURI} publishing to Property topic '${propertyName}' `); const buffer = await content.toBuffer(); + + if (this.broker === undefined) { + warn(`MqttBrokerServer at ${this.brokerURI} has no client to publish to. Probably it was closed.`); + return; + } + this.broker.publish(topic, buffer); }; thing.handleObserveProperty(propertyName, observeListener, { formIndex: property.forms.length - 1 }); @@ -158,6 +176,8 @@ export default class MqttBrokerServer implements ProtocolServer { } private exposeAction(name: string, actionName: string, thing: ExposedThing) { + MqttBrokerServer.brokerIsInitialized(this.broker); + const topic = encodeURIComponent(name) + "/actions/" + encodeURIComponent(actionName); this.broker.subscribe(topic); @@ -179,6 +199,11 @@ export default class MqttBrokerServer implements ProtocolServer { debug(`MqttBrokerServer at ${this.brokerURI} assigns '${href}' to Event '${eventName}'`); const eventListener = async (content: Content) => { + if (this.broker === undefined) { + warn(`MqttBrokerServer at ${this.brokerURI} has no client to publish to. Probably it was closed.`); + return; + } + if (!content) { warn(`MqttBrokerServer on port ${this.getPort()} cannot process data for Event ${eventName}`); thing.handleUnsubscribeEvent(eventName, eventListener, { formIndex: event.forms.length - 1 }); @@ -199,6 +224,9 @@ export default class MqttBrokerServer implements ProtocolServer { payload = rawPayload; } else if (typeof rawPayload === "string") { payload = Buffer.from(rawPayload); + } else { + warn(`MqttBrokerServer on port ${this.getPort()} received unexpected payload type`); + return; } if (segments.length === this.ACTION_SEGMENT_LENGTH) { @@ -308,7 +336,7 @@ export default class MqttBrokerServer implements ProtocolServer { error( `MqttBrokerServer at ${this.brokerURI} got error on writing to property '${ segments[this.INTERACTION_NAME_SEGMENT_INDEX] - }': ${err.message}` + }': ${err}` ); } } else { @@ -322,7 +350,8 @@ export default class MqttBrokerServer implements ProtocolServer { public async destroy(thingId: string): Promise { debug(`MqttBrokerServer on port ${this.getPort()} destroying thingId '${thingId}'`); - let removedThing: ExposedThing; + let removedThing: ExposedThing | undefined; + for (const name of Array.from(this.things.keys())) { const expThing = this.things.get(name); if (expThing != null && expThing.id != null && expThing.id === thingId) { @@ -330,6 +359,7 @@ export default class MqttBrokerServer implements ProtocolServer { removedThing = expThing; } } + if (removedThing) { info(`MqttBrokerServer succesfully destroyed '${removedThing.title}'`); } else { @@ -385,8 +415,12 @@ export default class MqttBrokerServer implements ProtocolServer { } if (this.hostedBroker !== undefined) { - await new Promise((resolve) => this.hostedServer.close(() => resolve())); - await new Promise((resolve) => this.hostedBroker.close(() => resolve())); + // When the broker is hosted, we need to close it. + // Both this.hostedBroker and this.hostedServer are defined at the same time. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await new Promise((resolve) => this.hostedServer!.close(() => resolve())); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await new Promise((resolve) => this.hostedBroker!.close(() => resolve())); } } @@ -394,7 +428,11 @@ export default class MqttBrokerServer implements ProtocolServer { return this.port; } - public getAddress(): string { + /** + * + * @returns the address of the broker or undefined if the Server is not started. + */ + public getAddress(): string | undefined { return this.address; } @@ -408,15 +446,15 @@ export default class MqttBrokerServer implements ProtocolServer { for (let i = 0; i < this.config.selfHostAuthentication.length; i++) { if ( username === this.config.selfHostAuthentication[i].username && - password.equals(Buffer.from(this.config.selfHostAuthentication[i].password)) + password.equals(Buffer.from(this.config.selfHostAuthentication[i].password ?? "")) ) { - done(undefined, true); + done(null, true); return; } } - done(undefined, false); + done(null, false); return; } - done(undefined, true); + done(null, true); } } diff --git a/packages/binding-mqtt/src/mqtt-client.ts b/packages/binding-mqtt/src/mqtt-client.ts index 2b1f2d15a..3c279a4d9 100644 --- a/packages/binding-mqtt/src/mqtt-client.ts +++ b/packages/binding-mqtt/src/mqtt-client.ts @@ -17,7 +17,7 @@ * Protocol test suite to test protocol implementations */ -import { ProtocolClient, Content, DefaultContent, createLoggers } from "@node-wot/core"; +import { ProtocolClient, Content, DefaultContent, createLoggers, ContentSerdes } from "@node-wot/core"; import * as TD from "@node-wot/td-tools"; import * as mqtt from "mqtt"; import { MqttClientConfig, MqttForm, MqttQoS } from "./mqtt"; @@ -40,7 +40,7 @@ export default class MqttClient implements ProtocolClient { this.scheme = "mqtt" + (secure ? "s" : ""); } - private client: mqtt.MqttClient = undefined; + private client?: mqtt.MqttClient; public subscribeResource( form: MqttForm, @@ -50,7 +50,7 @@ export default class MqttClient implements ProtocolClient { ): Promise { return new Promise((resolve, reject) => { // get MQTT-based metadata - const contentType = form.contentType; + const contentType = form.contentType ?? ContentSerdes.DEFAULT; const requestUri = new url.URL(form.href); const topic = requestUri.pathname.slice(1); const brokerUri: string = `${this.scheme}://` + requestUri.host; @@ -63,15 +63,29 @@ export default class MqttClient implements ProtocolClient { this.client.subscribe(topic); resolve( new Subscription(() => { + if (!this.client) { + warn( + `MQTT Client is undefined. This means that the client either failed to connect or was never initialized.` + ); + return; + } this.client.unsubscribe(topic); }) ); } this.client.on("connect", () => { - this.client.subscribe(topic); + // In this case, the client is definitely defined. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.client!.subscribe(topic); resolve( new Subscription(() => { + if (!this.client) { + warn( + `MQTT Client is undefined. This means that the client either failed to connect or was never initialized.` + ); + return; + } this.client.unsubscribe(topic); }) ); @@ -166,8 +180,13 @@ export default class MqttClient implements ProtocolClient { const security: TD.SecurityScheme = metadata[0]; if (security.scheme === "basic") { - this.config.username = credentials.username; - this.config.password = credentials.password; + if (credentials === undefined) { + // FIXME: This error message should be reworded and adapt to logging convention + throw new Error("binding-mqtt: security wants to be basic but you have provided no credentials"); + } else { + this.config.username = credentials.username; + this.config.password = credentials.password; + } } return true; } diff --git a/packages/binding-mqtt/src/mqtt.ts b/packages/binding-mqtt/src/mqtt.ts index b5e5418a4..459557c34 100644 --- a/packages/binding-mqtt/src/mqtt.ts +++ b/packages/binding-mqtt/src/mqtt.ts @@ -47,7 +47,7 @@ export class MqttForm extends Form { } export interface MqttClientConfig { - // username & password are redundated here (also find them in MqttClientSecurityParameters) + // username & password are redundant here (also find them in MqttClientSecurityParameters) // because MqttClient.setSecurity() method can inject authentication credentials into this interface // which will be then passed to mqtt.connect() once for all username?: string; diff --git a/packages/binding-mqtt/test/mqtt-client-subscribe-test.integration.ts b/packages/binding-mqtt/test/mqtt-client-subscribe-test.integration.ts index eaf490f96..6a98cfa47 100644 --- a/packages/binding-mqtt/test/mqtt-client-subscribe-test.integration.ts +++ b/packages/binding-mqtt/test/mqtt-client-subscribe-test.integration.ts @@ -76,6 +76,10 @@ describe("MQTT client implementation", () => { if (!eventReceived) { eventReceived = true; } else { + if (!x.data) { + done(new Error("No data received")); + return; + } ProtocolHelpers.readStreamFully(ProtocolHelpers.toNodeStream(x.data)).then( (received) => { expect(JSON.parse(received.toString())).to.equal(++check); @@ -134,6 +138,10 @@ describe("MQTT client implementation", () => { if (!eventReceived) { eventReceived = true; } else { + if (!x.data) { + done(new Error("No data received")); + return; + } ProtocolHelpers.readStreamFully(ProtocolHelpers.toNodeStream(x.data)).then( (received) => { expect(JSON.parse(received.toString())).to.equal(++check); diff --git a/packages/binding-mqtt/test/mqtt-client-subscribe-test.unit.ts b/packages/binding-mqtt/test/mqtt-client-subscribe-test.unit.ts index a734bd8fd..e889869a2 100644 --- a/packages/binding-mqtt/test/mqtt-client-subscribe-test.unit.ts +++ b/packages/binding-mqtt/test/mqtt-client-subscribe-test.unit.ts @@ -120,10 +120,10 @@ describe("MQTT client implementation", () => { beforeEach(() => { aedes.authenticate = function (_client, username: Readonly, password: Readonly, done) { if (username !== undefined) { - done(undefined, username === "user" && password.equals(Buffer.from("pass"))); + done(null, username === "user" && password.equals(Buffer.from("pass"))); return; } - done(undefined, true); + done(null, true); }; const server = net.createServer(aedes.handle); hostedBroker = server.listen(brokerPort); diff --git a/packages/binding-mqtt/tsconfig.json b/packages/binding-mqtt/tsconfig.json index e9fa7c062..df9cd1d90 100644 --- a/packages/binding-mqtt/tsconfig.json +++ b/packages/binding-mqtt/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "strict": true }, "include": ["src/**/*"], "references": [{ "path": "../td-tools" }, { "path": "../core" }] diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 0dbb667a2..46a0211d9 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -153,6 +153,7 @@ export default class Helpers implements Resolver { } // TODO: specialize fetch to retrieve just thing descriptions + // see https://github.com/eclipse-thingweb/node-wot/issues/1055 public fetch(uri: string): Promise { return new Promise((resolve, reject) => { const client = this.srv.getClientFor(Helpers.extractScheme(uri)); diff --git a/packages/examples/src/scripts/countdown.ts b/packages/examples/src/scripts/countdown.ts index 922880a2f..fa7c2e25c 100644 --- a/packages/examples/src/scripts/countdown.ts +++ b/packages/examples/src/scripts/countdown.ts @@ -13,8 +13,6 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import { InteractionOptions } from "wot-typescript-definitions"; - function uuidv4(): string { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; @@ -91,8 +89,8 @@ WoT.produce({ console.log("Update countdowns"); const listToDelete: string[] = []; for (const id of countdowns.keys()) { - const as: ActionStatus = countdowns.get(id); - if (as.output !== undefined) { + const as = countdowns.get(id); + if (as?.output !== undefined) { const prev = as.output; as.output--; console.log("\t" + id + ", from " + prev + " to " + as.output); @@ -117,20 +115,17 @@ WoT.produce({ }, 1000); // set property handlers (using async-await) - thing.setPropertyReadHandler( - "countdowns", - async (options: InteractionOptions): Promise => { - const cts: string[] = []; - for (const id of countdowns.keys()) { - cts.push(id); - } - return cts; + thing.setPropertyReadHandler("countdowns", async (options): Promise => { + const cts: string[] = []; + for (const id of countdowns.keys()) { + cts.push(id); } - ); + return cts; + }); // set action handlers (using async-await) thing.setActionHandler( "startCountdown", - async (params: WoT.InteractionOutput, options: InteractionOptions): Promise => { + async (params: WoT.InteractionOutput, options): Promise => { let initValue = 100; if (params) { const value = await params.value(); @@ -145,21 +140,25 @@ WoT.produce({ }; const ii: WoT.InteractionInput = resp; console.log("init countdown value = " + JSON.stringify(resp)); - countdowns.set(resp.href, resp); + countdowns.set(resp.href ?? "", resp); return ii; } ); thing.setActionHandler( "stopCountdown", - async (params: WoT.InteractionOutput, options: InteractionOptions): Promise => { + async (params: WoT.InteractionOutput, options): Promise => { if (params) { const value = await params.value(); if (typeof value === "string" && countdowns.has(value)) { - const as: ActionStatus = countdowns.get(value); - as.output = 0; - as.status = Status.completed; - console.log("Countdown stopped for href: " + value); - return undefined; + const as = countdowns.get(value); + if (as !== undefined) { + as.output = 0; + as.status = Status.completed; + console.log("Countdown stopped for href: " + value); + return null; + } else { + throw Error("Countdown value is undefined for href, " + value); + } } else { throw Error("Input provided for stopCountdown is no string or invalid href, " + value); } @@ -170,11 +169,11 @@ WoT.produce({ ); thing.setActionHandler( "monitorCountdown", - async (params: WoT.InteractionOutput, options: InteractionOptions): Promise => { + async (params: WoT.InteractionOutput, options): Promise => { if (params) { const value = await params.value(); if (typeof value === "string" && countdowns.has(value)) { - const as: ActionStatus = countdowns.get(value); + const as = countdowns.get(value); return JSON.stringify(as); } else { throw Error("Input provided for monitorCountdown is no string or invalid href, " + value); diff --git a/packages/examples/src/scripts/counter-client.ts b/packages/examples/src/scripts/counter-client.ts index 376c72366..6389b4dcb 100644 --- a/packages/examples/src/scripts/counter-client.ts +++ b/packages/examples/src/scripts/counter-client.ts @@ -16,13 +16,15 @@ import { Helpers } from "@node-wot/core"; import { ThingDescription } from "wot-typescript-definitions"; -let WoTHelpers: Helpers; +let WoTHelpers!: Helpers; function getFormIndexForDecrementWithCoAP(thing: WoT.ConsumedThing): number { - const forms = thing.getThingDescription().actions.decrement.forms; - for (let i = 0; i < forms.length; i++) { - if (/^coaps?:\/\/.*/.test(forms[i].href)) { - return i; + const forms = thing.getThingDescription().actions?.decrement.forms; + if (forms !== undefined) { + for (let i = 0; i < forms.length; i++) { + if (/^coaps?:\/\/.*/.test(forms[i].href)) { + return i; + } } } // return formIndex: 0 if no CoAP target IRI found diff --git a/packages/examples/src/scripts/counter.ts b/packages/examples/src/scripts/counter.ts index 4c6dda51b..c70545bdc 100644 --- a/packages/examples/src/scripts/counter.ts +++ b/packages/examples/src/scripts/counter.ts @@ -208,7 +208,7 @@ WoT.produce({ let fill = "black"; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("fill" in options.uriVariables) { + if (options.uriVariables && "fill" in options.uriVariables) { const uriVariables = options.uriVariables as Record; fill = uriVariables.fill; } @@ -234,7 +234,7 @@ WoT.produce({ let step = 1; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("step" in options.uriVariables) { + if (options.uriVariables && "step" in options.uriVariables) { const uriVariables = options.uriVariables as Record; step = uriVariables.step as number; } @@ -251,7 +251,7 @@ WoT.produce({ let step = 1; if (options && typeof options === "object" && "uriVariables" in options) { console.log("options = " + JSON.stringify(options)); - if ("step" in options.uriVariables) { + if (options.uriVariables && "step" in options.uriVariables) { const uriVariables = options.uriVariables as Record; step = uriVariables.step as number; } diff --git a/packages/examples/src/scripts/smart-coffee-machine-client.ts b/packages/examples/src/scripts/smart-coffee-machine-client.ts index b365146cc..05e745c35 100644 --- a/packages/examples/src/scripts/smart-coffee-machine-client.ts +++ b/packages/examples/src/scripts/smart-coffee-machine-client.ts @@ -19,7 +19,7 @@ import { ThingDescription } from "wot-typescript-definitions"; import { Helpers } from "@node-wot/core"; -let WoTHelpers: Helpers; +let WoTHelpers!: Helpers; // Print data and an accompanying message in a distinguishable way function log(msg: string, data: unknown) { @@ -60,7 +60,7 @@ WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) = const makeCoffee = await thing.invokeAction("makeDrink", undefined, { uriVariables: { drinkId: "latte", size: "l", quantity: 3 }, }); - const makeCoffeep = (await makeCoffee.value()) as Record; + const makeCoffeep = (await makeCoffee?.value()) as Record; if (makeCoffeep.result) { log("Enjoy your drink!", makeCoffeep); } else { @@ -79,7 +79,7 @@ WoTHelpers.fetch("http://127.0.0.1:8080/smart-coffee-machine").then(async (td) = time: "10:00", mode: "everyday", }); - const scheduledTaskp = (await scheduledTask.value()) as Record; + const scheduledTaskp = (await scheduledTask?.value()) as Record; log(scheduledTaskp.message, scheduledTaskp); // See how it has been added to the schedules property diff --git a/packages/examples/src/security/oauth/consumer.ts b/packages/examples/src/security/oauth/consumer.ts index 8288b6622..ccb16fb29 100644 --- a/packages/examples/src/security/oauth/consumer.ts +++ b/packages/examples/src/security/oauth/consumer.ts @@ -15,12 +15,13 @@ import { Helpers } from "@node-wot/core"; import { ThingDescription } from "wot-typescript-definitions"; -let WoTHelpers: Helpers; +let WoTHelpers!: Helpers; WoTHelpers.fetch("https://localhost:8080/oauth").then((td) => { WoT.consume(td as ThingDescription).then(async (thing) => { try { - const result = await (await thing.invokeAction("sayOk")).value(); + const resp = await thing.invokeAction("sayOk"); + const result = resp?.value(); console.log("oAuth token was", result); } catch (error) { console.log("It seems that I couldn't access the resource"); diff --git a/packages/examples/src/testthing/testclient.ts b/packages/examples/src/testthing/testclient.ts index 3abab1967..ad2315975 100644 --- a/packages/examples/src/testthing/testclient.ts +++ b/packages/examples/src/testthing/testclient.ts @@ -15,7 +15,7 @@ import { Helpers } from "@node-wot/core"; import { ThingDescription } from "wot-typescript-definitions"; -let WoTHelpers: Helpers; +let WoTHelpers!: Helpers; console.log = () => { /* empty */ @@ -30,7 +30,7 @@ async function testPropertyRead(thing: WoT.ConsumedThing, name: string) { const value = await res.value(); console.info("PASS " + name + " READ:", value); } catch (err) { - console.error("FAIL " + name + " READ:", err.message); + console.error("FAIL " + name + " READ:", JSON.stringify(err)); } } @@ -46,8 +46,8 @@ async function testPropertyWrite( if (!shouldFail) console.info("PASS " + name + " WRITE (" + displayValue + ")"); else console.error("FAIL " + name + " WRITE: (" + displayValue + ")"); } catch (err) { - if (!shouldFail) console.error("FAIL " + name + " WRITE (" + displayValue + "):", err.message); - else console.info("PASS " + name + " WRITE (" + displayValue + "):", err.message); + if (!shouldFail) console.error("FAIL " + name + " WRITE (" + displayValue + "):", JSON.stringify(err)); + else console.info("PASS " + name + " WRITE (" + displayValue + "):", JSON.stringify(err)); } } diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json index bfabb0e56..61b6c09bc 100644 --- a/packages/examples/tsconfig.json +++ b/packages/examples/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "strict": true, "outDir": "dist", "rootDir": "src", "target": "ES2018", - "alwaysStrict": false, - "noImplicitUseStrict": true, "sourceMap": false, "removeComments": false },