diff --git a/packages/binding-modbus/README.md b/packages/binding-modbus/README.md index 0cba02bf7..b19208fba 100644 --- a/packages/binding-modbus/README.md +++ b/packages/binding-modbus/README.md @@ -3,62 +3,98 @@ ## Overview W3C Web of Things (WoT) Protocol Binding for [Modbus](https://en.wikipedia.org/wiki/Modbus) TCP [RFC](https://tools.ietf.org/html/draft-dube-modbus-applproto-00). -This package uses [modbus-serial](https://www.npmjs.com/package/modbus-serial) as a low level client for Modbus TCP. +This package uses [modbus-serial](https://www.npmjs.com/package/modbus-serial) as a low-level client for Modbus TCP. W3C WoT Binding Template for Modbus can be found [here](https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/index.html). Current Maintainer(s): [@relu91](https://github.com/relu91) [@fillobotto](https://github.com/fillobotto) -## Protocol specifier +## Client Example + +You can use a code like the following to use the binding. This specific code is interacting with one of the Eclipse Thingweb Test Things at . + +```js +// Protocols and Servient +Servient = require("@node-wot/core").Servient; +ModbusClientFactory = require("@node-wot/binding-modbus").ModbusClientFactory; + +// create Servient and add Modbus binding +let servient = new Servient(); +servient.addClientFactory(new ModbusClientFactory()); + +async function main() { + let td = {}; // see https://github.com/eclipse-thingweb/test-things/blob/main/things/elevator/modbus/js/modbus-elevator.td.json + + const WoT = await servient.start(); + const thing = await WoT.consume(td); + + const readData1 = await thing.readProperty("lightSwitch"); // coil + const readData2 = await thing.readProperty("floorNumber"); // register + + const readValue1 = await readData1.value(); + console.log(readValue1); + + const readValue2 = await readData2.value(); + console.log(readValue2); +} + +main(); +``` + +## Binding Information + +### Protocol specifier The protocol prefix handled by this binding is `modbus+tcp://`. -## New Form Fields for the Modbus Binding +### New Form Fields for the Modbus Binding **Note**: for further details please refer to the [documentation](https://github.com/eclipse-thingweb/node-wot/blob/master/packages/binding-modbus/src/modbus.ts). -### modbus:function +#### modv:function + +Specifies which Modbus function should be issued in the requested command. The list of the available functions is the following: + +| Function ID | Label | +| ----------- | :---------------------------: | +| 1 | readCoil | +| 2 | readDiscreteInput | +| 3 | readHoldingRegisters | +| 4 | readInputRegisters | +| 5 | writeSingleCoil | +| 6 | writeSingleHoldingRegister | +| 15 | writeMultipleCoils | +| 16 | writeMultipleHoldingRegisters | -Specifies which modbus function should be issue in the requested command. The list of the available function is the following: -| Function ID | Label -| ------------- |:--------------------:| -| 1 | readCoil | -| 2 | readDiscreteInput | -| 3 | readHoldingRegisters | -| 4 | readInputRegisters | -| 5 | writeSingleCoil | -| 6 | writeSingleHoldingRegister | -| 15 | writeMultipleCoils | -| 16 | writeMultipleHoldingRegisters | -The list is build from [wikipedia definition](https://en.wikipedia.org/wiki/Modbus) of modbus function names, it can be different in proprietary implementations of modbus protocol. -Function IDs or labels can be either used as values of `modbus:function` property. +This list is from [the Modbus Binding Template](https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/#function). +Function IDs are used on the wire but the labels should be used in a TD as the values of `modv:function` property. -### modbus:entity +#### modv:entity -A more user-friendly property to specify `modbus:function`. It can be filled with the following keywords: `Coil`, `DiscreteInput`, `InputRegister`, `HoldingRegister`. The client will then determine the right function code to be applied in the modbus request. Futhermore, it can be used in multi-operation forms whereas modbus:function cannot. See the [example] +The Modbus resource the `modv:function` acts on. It can be filled with the following keywords: `Coil`, `DiscreteInput`, `InputRegister`, `HoldingRegister`. The client will then determine the right function code to be applied in the Modbus request. Furthermore, it can be used in multi-operation forms where `modv:function` cannot be used but the correct function is filled based on [the default assumptions](https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/#default-mappings). See the [example]. -**Notice** that when used in conjunction with `modbus:function`, the value of `modbus:function` property is ignored. +**Note** that when used in conjunction with `modv:function`, the value of `modv:entity` property is ignored. -### modbus:unitID +#### modv:unitID -The physical bus address of the unit targeted by the modbus request. +The physical bus address of the unit targeted by the Modbus request. -### modbus:address +#### modv:address This property defines the starting address of registers or coils that are meant to be written. -### modbus:quantity +#### modv:quantity -This property defines the total amount of registers or coils that should be written, beginning with the register specified with the property 'modbus:address'. +This property defines the total amount of registers or coils that should be written, beginning with the register specified with the property `modv:address`. -### modbus:pollingTime +#### modv:pollingTime -The polling time used for subscriptions. The client will issue a reading command every modbus:pollingTime milliseconds. Note that the reading request timeout can be still controlled using modbus:timeout property. +The polling time used for subscriptions. The client will issue a reading command every `modv:pollingTime` milliseconds. Note that the reading request timeout can be still controlled using `modv:timeout` property. -### modbus:timeout +#### modv:timeout -Timeout in milliseconds of the modbus request. Default to 1000 milliseconds +Timeout in milliseconds of the Modbus request. Default to 1000 milliseconds -## URL format +### URL format The URL is used to transport all addressing information necessary to describe the MODBUS connection and register addresses. It has the following structure: @@ -68,17 +104,17 @@ modbus+tcp:// [ : ] [/ [ /
] [&quantity=` is the host name or IP address of the MODBUS slave +- `` is the hostname or IP address of the MODBUS slave - `` is the optional TCP port number used to access the MODBUS slave. Default is 502 -- `` is the MODBUS unit id of the MODBUS slave; same as [modbus:unitID](#modbus:unitID) -- `
` is the starting address register number; see [modbus:address](#modbus:address) -- `` is the optional number of registers to access. Default is 1; see [modbus:quantity](#modbus:quantity) +- `` is the MODBUS unit id of the MODBUS slave; same as [modv:unitID](#modv:unitID) +- `
` is the starting address register number; see [modv:address](#modv:address) +- `` is the optional number of registers to access. Default is 1; see [modv:quantity](#modv:quantity) When specified URL values override the corresponding `form` parameter. -## DataSchema +### DataSchema -The MODBUS binding uses and provides plain binary data for reading and writing. Therefore in most cases it will be associated with the content type `application/octet-stream`. Please refer to the description of this codec on how to decode and encode plain register values to/from JavaScript objects (See `OctetstreamCodec`). **Note** `array` and `object` schema are not supported. +The MODBUS binding uses and provides plain binary data for reading and writing. Therefore in most cases, it will be associated with the content type `application/octet-stream`. Please refer to the description of this codec on how to decode and encode plain register values to/from JavaScript objects (See `OctetstreamCodec`). **Note** `array` and `object` schema are not supported. Along with content type `application/octet-stream`, this protocol binding accepts also an optional `byteSeq` parameter. `byteSeq` specifies the endian-ness of the raw byte data being read/written by the MODBUS binding. It follows the notation `application/octet-stream;byteSeq=value`, where its value can be one of the following: @@ -87,7 +123,7 @@ Along with content type `application/octet-stream`, this protocol binding accept - `BIG_ENDIAN_BYTE_SWAP` - `LITTLE_ENDIAN_BYTE_SWAP` -**Note**: the list may be extended in the future. +**Note**: the list above may be extended in the future. In particular, the decimal numbers `9545` and `22880` will be encoded as follows: @@ -96,45 +132,45 @@ In particular, the decimal numbers `9545` and `22880` will be encoded as follows - `BIG_ENDIAN_BYTE_SWAP`: `49 25 60 59` - `LITTLE_ENDIAN_BYTE_SWAP`: `59 60 25 49` -For register properties the payload is just the plain sequence of bytes read from or written to the registers. For coils and discrete inputs, the payload is a sequence of bytes, each corresponding to a single coil or discrete input. Each byte contains the value `0` or `1`. So the encoder / decoder should work on this series of bytes and does not have to take care about handling the individual bits. Mapping each coil or discrete input to a single property of type `boolean` works just fine! +For register resources, the payload is just the plain sequence of bytes read from or written to the registers. For coils and discrete inputs, the payload is a sequence of bytes, each corresponding to a single coil or discrete input. Each byte contains the value `0` or `1`. So the encoder and decoder should work on this series of bytes and does not have to take care about handling the individual bits. Mapping each coil or discrete input to a single property of type `boolean` works just fine. Another parameter that can be used in conjunction with `application/octet-stream` is `length`. This parameter specifies the length of the payload in bytes. This is useful to indicate the actual length of the payload when reading or writing a sequence of registers. For example, when reading a property using `readHoldingRegisters`, the payload length can be used to specify the number of registers to be read. Notice that the payload length must always -be equal to the number of registers multiplied by the registers size in bytes. +be equal to the number of registers multiplied by the size of the register in bytes. -## Security +### Security The protocol does not support security. -# Implementation notes +## Implementation notes This implementation handles multiple requests to the same slave by serializing and combining them if possible. In the following, the terms **request** and **transaction** are used as follows to describe this: - A **request** is a read or write request to a resource as issued by a user of the node-wot API. -- A **transaction** is a MODBUS operation and may cover the data of multiple **requests**. +- A **transaction** is a Modbus operation and may cover the data of multiple **requests**. -## Combination +### Combination -When two **requests** of the same type are issued and these requests cover neighboured registers, then they are combined into a single **transaction** reading or writing the combined register range. Note that this algorithm is currently rather simple: New **requests** are just checked if they can be prepended or appended to an existing **transaction**. If not, a new **transaction** is created. When a **transaction** completes, all **requests** contained in this **transaction** are completed. +When two **requests** of the same type are issued and these requests cover neighbored registers, then they are combined into a single **transaction** reading or writing the combined register range. Note that this algorithm is currently rather simple: New **requests** are just checked if they can be prepended or appended to an existing **transaction**. If not, a new **transaction** is created. When a **transaction** completes, all **requests** contained in this **transaction** are completed. -## Serialization +### Serialization Multiple **transactions** to the same slave are serialized. This means that a new MODBUS **transaction** is only started when the previous **transaction** was finished. **Transactions** are held in a queue and executed in a first-come-first-serve manner. -## Combination using the node-wot API +### Combination using the node-wot API -To help the MODBUS binding to perform combination a user of the API should create several requests for neighboured registers and resolve them all together in a single call to `Promise.all()`, e.g. as follows: +To help the MODBUS binding to perform combination a user of the API should create several requests for neighbor registers and resolve them all together in a single call to `Promise.all()`, e.g. as follows: ```javascript console.info("Creating promise vl1n"); -let vl1n = pac3200.properties["Voltage/L1N"].read(); +let vl1n = pac3200Thing.readProperty("Voltage/L1N"); console.info("Creating promise vl2n"); -let vl2n = pac3200.properties["Voltage/L2N"].read(); +let vl2n = pac3200Thing.readProperty("Voltage/L2N"); console.info("Creating promise vl3n"); -let vl3n = pac3200.properties["Voltage/L3N"].read(); +let vl3n = pac3200Thing.readProperty("Voltage/L3N"); console.info("Resolving all promises"); Promise.all([vl1n, vl2n, vl3n]) - .then((values) => values.forEach((v) => console.info("Voltage = ", v))) + .then((values) => values.forEach((v) => console.info("Voltage = ", await v.value()))) .catch((reason) => console.warn("Failed ", reason)); ``` @@ -148,13 +184,11 @@ Reads the 8th input register of the unit 1 ```json { - "href": "modbus://127.0.0.1:60000", + "href": "modbus+tcp://127.0.0.1:60000/1/8", "contentType": "application/octet-stream;length=2", "op": ["readproperty"], - "modbus:function": "readInputRegister", - "modbus:address": 8, - "modbus:unitID": 1, - "modbus:timeout": 2000 + "modv:function": "readInputRegister", + "modv:timeout": 2000 } ``` @@ -164,12 +198,10 @@ Read and write the 8th holding register of the unit 1 ```json { - "href": "modbus://127.0.0.1:60000", + "href": "modbus+tcp://127.0.0.1:60000/1/8", "contentType": "application/octet-stream;length=2", "op": ["readproperty", "writeproperty"], - "modbus:entity": "HoldingRegister", - "modbus:address": 8, - "modbus:unitID": 1 + "modv:entity": "HoldingRegister" } ``` @@ -179,13 +211,11 @@ Polls the 8th holding register of the unit 1 every second. ```json { - "href": "modbus://127.0.0.1:60000", + "href": "modbus+tcp://127.0.0.1:60000/1/8", "contentType": "application/octet-stream;length=2", "op": ["observeproperty"], - "modbus:entity": "HoldingRegister", - "modbus:address": 8, - "modbus:unitID": 1, - "modbus:pollingTime": 1000 + "modv:entity": "HoldingRegister", + "modv:pollingTime": 1000 } ``` @@ -202,4 +232,4 @@ Polls the 8th holding register of the unit 1 every second. ## More Details -See +See . diff --git a/packages/binding-modbus/src/modbus-client.ts b/packages/binding-modbus/src/modbus-client.ts index 400c2d531..b4f5830ad 100644 --- a/packages/binding-modbus/src/modbus-client.ts +++ b/packages/binding-modbus/src/modbus-client.ts @@ -55,7 +55,7 @@ class ModbusSubscription { } clearInterval(this.interval); } - }, form["modbus:pollingTime"]); + }, form["modv:pollingTime"]); // TODO: Until https://github.com/eclipse-thingweb/node-wot/issues/1236 is clarified, we will use this as the polling rate. this.complete = complete; } @@ -86,13 +86,13 @@ export default class ModbusClient implements ProtocolClient { async invokeResource(form: ModbusForm, content: Content): Promise { await this.performOperation(form, content); - // As mqtt there is no response + // Same with MQTT, there is no response return new DefaultContent(Readable.from("")); } unlinkResource(form: ModbusForm): Promise { form = this.validateAndFillDefaultForm(form, 0); - const id = `${form.href}/${form["modbus:unitID"]}#${form["modbus:function"]}?${form["modbus:address"]}&${form["modbus:quantity"]}`; + const id = `${form.href}/${form["modv:unitID"]}#${form["modv:function"]}?${form["modv:address"]}&${form["modv:quantity"]}`; const subscription = this._subscriptions.get(id); if (!subscription) { @@ -114,7 +114,7 @@ export default class ModbusClient implements ProtocolClient { return new Promise((resolve, reject) => { form = this.validateAndFillDefaultForm(form, 0); - const id = `${form.href}/${form["modbus:unitID"]}#${form["modbus:function"]}?${form["modbus:address"]}&${form["modbus:quantity"]}`; + const id = `${form.href}/${form["modv:unitID"]}#${form["modv:function"]}?${form["modv:address"]}&${form["modv:quantity"]}`; if (this._subscriptions.has(id)) { reject(new Error("Already subscribed for " + id + ". Multiple subscriptions are not supported")); @@ -178,7 +178,7 @@ export default class ModbusClient implements ProtocolClient { debug(`Creating new ModbusConnection for ${hostAndPort}`); connection = new ModbusConnection(host, port, { - connectionTimeout: form["modbus:timeout"] ?? DEFAULT_TIMEOUT, + connectionTimeout: form["modv:timeout"] ?? DEFAULT_TIMEOUT, }); this._connections.set(hostAndPort, connection); } else { @@ -212,22 +212,28 @@ export default class ModbusClient implements ProtocolClient { return endianness; } - private overrideFormFromURLPath(input: ModbusForm) { + // This generates a form used internally with url content based on the uri scheme + // Ideally, more code should be refactored to use uri only + private addFormElementsFromURLPath(input: ModbusForm): ModbusForm { + const returnForm: ModbusForm = { ...input }; const { pathname, searchParams: query } = new URL(input.href); const pathComp = pathname.split("/"); - - input["modbus:unitID"] = parseInt(pathComp[1], 10) || input["modbus:unitID"]; - input["modbus:address"] = parseInt(pathComp[2], 10) || input["modbus:address"]; + if (pathComp.length < 3 || pathComp[1] === "" || pathComp[2] === "") { + throw new Error("Malformed href: unitID and address must be defined"); + } + returnForm["modv:unitID"] = parseInt(pathComp[1], 10); + returnForm["modv:address"] = parseInt(pathComp[2], 10); const queryQuantity = query.get("quantity"); if (queryQuantity != null) { - input["modbus:quantity"] = parseInt(queryQuantity, 10); + returnForm["modv:quantity"] = parseInt(queryQuantity, 10); } + return returnForm; } private validateBufferLength(form: ModbusFormWithDefaults, buffer: Buffer) { - const mpy = form["modbus:entity"] === "InputRegister" || form["modbus:entity"] === "HoldingRegister" ? 2 : 1; - const quantity = form["modbus:quantity"]; + const mpy = form["modv:entity"] === "InputRegister" || form["modv:entity"] === "HoldingRegister" ? 2 : 1; + const quantity = form["modv:quantity"]; if (buffer.length !== mpy * quantity) { throw new Error( "Content length does not match register / coil count, got " + @@ -239,35 +245,35 @@ export default class ModbusClient implements ProtocolClient { } } - private validateAndFillDefaultForm(form: ModbusForm, contentLength = 0): ModbusFormWithDefaults { + private validateAndFillDefaultForm(inputForm: 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) - this.overrideFormFromURLPath(form); + // Use URI values to generate form keys + const filledForm: ModbusForm = this.addFormElementsFromURLPath(inputForm); // take over latest content of form into a new result set - const result: ModbusForm = { ...form }; + const result: ModbusForm = { ...filledForm }; - if (form["modbus:function"] == null && form["modbus:entity"] == null) { - throw new Error("Malformed form: modbus:function or modbus:entity must be defined"); + if (filledForm["modv:function"] == null && filledForm["modv:entity"] == null) { + throw new Error("Malformed form: modv:function or modv:entity must be defined"); } - if (form["modbus:function"] != null) { + if (filledForm["modv:function"] != null) { // Convert string function to enums if defined - if (typeof form["modbus:function"] === "string") { - result["modbus:function"] = ModbusFunction[form["modbus:function"]]; + if (typeof filledForm["modv:function"] === "string") { + result["modv:function"] = ModbusFunction[filledForm["modv:function"]]; } // Check if the function is a valid modbus function code - if (!Object.keys(ModbusFunction).includes(form["modbus:function"].toString())) { - throw new Error("Undefined function number or name: " + form["modbus:function"]); + if (!Object.keys(ModbusFunction).includes(filledForm["modv:function"].toString())) { + throw new Error("Undefined function number or name: " + filledForm["modv:function"]); } } - if (form["modbus:entity"]) { - switch (form["modbus:entity"]) { + if (filledForm["modv:entity"]) { + switch (filledForm["modv:entity"]) { case "Coil": - result["modbus:function"] = + result["modv:function"] = mode === "r" ? ModbusFunction.readCoil : contentLength > 1 @@ -276,7 +282,7 @@ export default class ModbusClient implements ProtocolClient { break; case "HoldingRegister": // the content length must be divided by 2 (holding registers are 16bit) - result["modbus:function"] = + result["modv:function"] = mode === "r" ? ModbusFunction.readHoldingRegisters : contentLength / 2 > 1 @@ -284,35 +290,35 @@ export default class ModbusClient implements ProtocolClient { : ModbusFunction.writeSingleHoldingRegister; break; case "InputRegister": - result["modbus:function"] = ModbusFunction.readInputRegister; + result["modv:function"] = ModbusFunction.readInputRegister; break; case "DiscreteInput": - result["modbus:function"] = ModbusFunction.readDiscreteInput; + result["modv:function"] = ModbusFunction.readDiscreteInput; break; default: - throw new Error("Unknown modbus entity: " + form["modbus:entity"]); + throw new Error("Unknown modbus entity: " + filledForm["modv:entity"]); } } else { - // 'modbus:entity' undefined but modbus:function defined - result["modbus:entity"] = modbusFunctionToEntity(result["modbus:function"] as ModbusFunction); + // 'modv:entity' undefined but modv:function defined + result["modv:entity"] = modbusFunctionToEntity(result["modv:function"] as ModbusFunction); } - if (form["modbus:address"] === undefined || form["modbus:address"] === null) { + if (filledForm["modv:address"] === undefined || filledForm["modv:address"] === null) { throw new Error("Malformed form: address must be defined"); } - const hasQuantity = form["modbus:quantity"] != null; + const hasQuantity = filledForm["modv:quantity"] != null; if (!hasQuantity && contentLength === 0) { - result["modbus:quantity"] = 1; + result["modv:quantity"] = 1; } else if (!hasQuantity && contentLength > 0) { const regSize = - result["modbus:entity"] === "InputRegister" || result["modbus:entity"] === "HoldingRegister" ? 2 : 1; - result["modbus:quantity"] = contentLength / regSize; + result["modv:entity"] === "InputRegister" || result["modv:entity"] === "HoldingRegister" ? 2 : 1; + result["modv:quantity"] = contentLength / regSize; } - result["modbus:pollingTime"] ??= DEFAULT_POLLING; - result["modbus:timeout"] ??= DEFAULT_TIMEOUT; + result["modv:pollingTime"] ??= DEFAULT_POLLING; + result["modv:timeout"] ??= DEFAULT_TIMEOUT; return result as ModbusFormWithDefaults; } diff --git a/packages/binding-modbus/src/modbus-connection.ts b/packages/binding-modbus/src/modbus-connection.ts index 11509e55f..71cc813f7 100644 --- a/packages/binding-modbus/src/modbus-connection.ts +++ b/packages/binding-modbus/src/modbus-connection.ts @@ -123,13 +123,13 @@ export type ModbusFormWithDefaults = ModbusForm & Required< Pick< ModbusForm, - | "modbus:function" - | "modbus:entity" - | "modbus:unitID" - | "modbus:address" - | "modbus:quantity" - | "modbus:timeout" - | "modbus:pollingTime" + | "modv:function" + | "modv:entity" + | "modv:unitID" + | "modv:address" + | "modv:quantity" + | "modv:timeout" + | "modv:pollingTime" > >; @@ -445,11 +445,11 @@ export class PropertyOperation { reject?: (reason?: Error) => void; 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.unitId = form["modv:unitID"]; + this.registerType = form["modv:entity"]; + this.base = form["modv:address"]; + this.quantity = form["modv:quantity"]; + this.function = form["modv:function"] as ModbusFunction; this.endianness = endianness; this.contentType = form.contentType ?? ContentSerdes.DEFAULT; this.content = content; diff --git a/packages/binding-modbus/src/modbus.ts b/packages/binding-modbus/src/modbus.ts index 409ceafd2..e3986191f 100644 --- a/packages/binding-modbus/src/modbus.ts +++ b/packages/binding-modbus/src/modbus.ts @@ -29,11 +29,12 @@ export enum ModbusFunction { "writeSingleHoldingRegister" = 6, "writeMultipleCoils" = 15, "writeMultipleHoldingRegisters" = 16, + "readDeviceIdentification" = 43, } /** * Different modbus function names as defined in - * https://en.wikipedia.org/wiki/Modbus. + * https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/#function. */ export type ModbusFunctionName = | "readCoil" @@ -42,45 +43,81 @@ export type ModbusFunctionName = | "writeSingleCoil" | "writeSingleHoldingRegister" | "writeMultipleCoils" - | "writeMultipleHoldingRegisters"; + | "writeMultipleHoldingRegisters" + | "readDeviceIdentification"; export class ModbusForm extends Form { /** * The modbus function issued in the request. */ - public "modbus:function"?: ModbusFunction | ModbusFunctionName; + public "modv:function"?: ModbusFunction | ModbusFunctionName; /** * Describe the entity type of the request. This property can be - * used instead of 'modbus:fuction' when the form has multiple op. For - * example if op = ['readProperty','writeProperty'] and 'modbus:function + * used instead of 'modv:function' when the form has multiple op. For + * example if op = ['readProperty','writeProperty'] and 'modv:function * is 'Coil', the low level modbus function will be mapped to 1 when * reading and to 5 when writing. */ - public "modbus:entity"?: ModbusEntity; + public "modv:entity"?: ModbusEntity; /** * Physical address of the unit connected to the bus. */ - public "modbus:unitID"?: number; + public "modv:unitID"?: number; /** * Defines the starting address of registers or coils that are * meant to be written. */ - public "modbus:address"?: number; + public "modv:address"?: number; /** * Defines the total amount of registers or coils that * should be written, beginning with the register specified * with the property 'modbus:address'. */ - public "modbus:quantity"?: number; + public "modv:quantity"?: number; + /** + * Maximum polling rate that this implementation uses for subscriptions. + * The client will issue a reading + * command every modv:pollingTime milliseconds. Note that + * the reading request timeout can be still controlled using + * modv:timeout property. + */ + public "modv:pollingTime"?: number; + /** + * When true, it describes that the byte order of the data in the Modbus message is the most significant byte first (i.e., Big-Endian). When false, it describes the least significant byte first (i.e., Little-Endian). + */ + public "modv:mostSignificantByte"?: boolean; + /** + * When true, it describes that the word order of the data in the Modbus message is the most significant word first (i.e., no word swapping). When false, it describes the least significant word first (i.e. word swapping) + */ + public "modv:mostSignificantWord"?: boolean; + /** + * Modbus implementations can differ in the way addressing works, as the first coil/register can be either referred to as true or false. + */ + public "modv:zeroBasedAddressing"?: boolean; + /** * Timeout in milliseconds of the modbus request. Default to 1000 milliseconds */ - public "modbus:timeout"?: number; + public "modv:timeout"?: number; /** - * Used for subscriptions. The client will issue a reading - * command every modbus:pollingTime milliseconds. Note that - * the reading request timeout can be still controlled using - * modbus:timeout property. + * Specifies the data type contained in the request or response payload. Users can define the specific data type using a sub type of xsd:decimal. */ - public "modbus:pollingTime"?: number; + public "modv:type"?: ModbusDataType; } + +export type ModbusDataType = + | "xsd:integer" + | "xsd:boolean" + | "xsd:string" + | "xsd:float" + | "xsd:decimal" + | "xsd:byte" + | "xsd:short" + | "xsd:int" + | "xsd:long" + | "xsd:unsignedByte" + | "xsd:unsignedShort" + | "xsd:unsignedInt" + | "xsd:unsignedLong" + | "xsd:double" + | "xsd:hexBinary"; diff --git a/packages/binding-modbus/test/modbus-client-test.ts b/packages/binding-modbus/test/modbus-client-test.ts index aa7e7793c..5bd936dbf 100644 --- a/packages/binding-modbus/test/modbus-client-test.ts +++ b/packages/binding-modbus/test/modbus-client-test.ts @@ -55,38 +55,29 @@ describe("Modbus client test", () => { }); it("use entity alias for coil", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:entity": "Coil", - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:entity": "Coil", }; /* eslint-disable dot-notation */ - client["validateAndFillDefaultForm"](form, 0)["modbus:function"].should.be.equal(1, "Wrong default read Coil"); - client["validateAndFillDefaultForm"](form, 1)["modbus:function"].should.be.equal(5, "Wrong write Coil"); - client["validateAndFillDefaultForm"](form, 2)["modbus:function"].should.be.equal( - 15, - "Wrong write multiple Coil" - ); + client["validateAndFillDefaultForm"](form, 0)["modv:function"].should.be.equal(1, "Wrong default read Coil"); + client["validateAndFillDefaultForm"](form, 1)["modv:function"].should.be.equal(5, "Wrong write Coil"); + client["validateAndFillDefaultForm"](form, 2)["modv:function"].should.be.equal(15, "Wrong write multiple Coil"); /* eslint-enable dot-notation */ }); it("use entity alias for holding registries", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:entity": "HoldingRegister", - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0/?quantity=1", + "modv:entity": "HoldingRegister", }; /* eslint-disable dot-notation */ - client["validateAndFillDefaultForm"](form)["modbus:function"].should.be.equal(3, "Wrong read Holding register"); - client["validateAndFillDefaultForm"](form, 2)["modbus:function"].should.be.equal( + client["validateAndFillDefaultForm"](form)["modv:function"].should.be.equal(3, "Wrong read Holding register"); + client["validateAndFillDefaultForm"](form, 2)["modv:function"].should.be.equal( 6, "Wrong write Holding register" ); - client["validateAndFillDefaultForm"](form, 4)["modbus:function"].should.be.equal( + client["validateAndFillDefaultForm"](form, 4)["modv:function"].should.be.equal( 16, "Wrong write multiple Holding register" ); @@ -95,71 +86,49 @@ describe("Modbus client test", () => { it("use entity alias for other entities", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:entity": "DiscreteInput", - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:entity": "DiscreteInput", }; /* eslint-disable dot-notation */ - client["validateAndFillDefaultForm"](form)["modbus:function"].should.be.equal(2, "Wrong read Discrete input"); - form["modbus:entity"] = "InputRegister"; - client["validateAndFillDefaultForm"](form)["modbus:function"].should.be.equal(4, "Wrong read Input register"); + client["validateAndFillDefaultForm"](form)["modv:function"].should.be.equal(2, "Wrong read Discrete input"); + form["modv:entity"] = "InputRegister"; + client["validateAndFillDefaultForm"](form)["modv:function"].should.be.equal(4, "Wrong read Input register"); /* eslint-enable dot-notation */ }); it("should convert function names", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": "readCoil", - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:function": "readCoil", }; // eslint-disable-next-line dot-notation - client["validateAndFillDefaultForm"](form)["modbus:function"].should.be.equal(1, "Wrong substitution"); + client["validateAndFillDefaultForm"](form)["modv:function"].should.be.equal(1, "Wrong substitution"); }); it("should accept just URL parameters", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502/2/2?quantity=5", - "modbus:function": "readCoil", + href: "modbus+tcp://127.0.0.1:8502/2/2?quantity=5", + "modv:function": "readCoil", }; // eslint-disable-next-line dot-notation const result = client["validateAndFillDefaultForm"](form); - result["modbus:unitID"].should.be.equal(2, "unitID value not set"); - result["modbus:address"].should.be.equal(2, "address value not set"); - result["modbus:quantity"].should.be.equal(5, "quantity value not set"); + result["modv:unitID"].should.be.equal(2, "unitID value not set"); + result["modv:address"].should.be.equal(2, "address value not set"); + result["modv:quantity"].should.be.equal(5, "quantity value not set"); }); it("should accept just URL parameters without quantity", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502/2/2", - "modbus:function": "readCoil", - }; - - // eslint-disable-next-line dot-notation - const result = client["validateAndFillDefaultForm"](form); - result["modbus:unitID"].should.be.equal(2, "unitID value not set"); - result["modbus:address"].should.be.equal(2, "address value not set"); - }); - - it("should override correctly form values with URL", () => { - const form: ModbusForm = { - href: "modbus://127.0.0.1:8502/2/2?quantity=5", - "modbus:function": "readCoil", - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/2/2", + "modv:function": "readCoil", }; // eslint-disable-next-line dot-notation const result = client["validateAndFillDefaultForm"](form); - result["modbus:unitID"].should.be.equal(2, "unitID value not overridden"); - result["modbus:address"].should.be.equal(2, "address value not overridden"); - result["modbus:quantity"].should.be.equal(5, "quantity value not overridden"); + result["modv:unitID"].should.be.equal(2, "unitID value not set"); + result["modv:address"].should.be.equal(2, "address value not set"); }); describe("misc", () => { @@ -167,11 +136,8 @@ describe("Modbus client test", () => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:function": 1, }; let result = await client.readResource(form); @@ -183,12 +149,9 @@ describe("Modbus client test", () => { }); it("should fail for timeout", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 4444, - "modbus:quantity": 1, - "modbus:unitID": 1, - "modbus:timeout": 100, + href: "modbus+tcp://127.0.0.1:8502/1/4444?quantity=1", + "modv:function": 1, + "modv:timeout": 100, }; await client.readResource(form).should.eventually.be.rejectedWith("Timed out"); @@ -199,11 +162,8 @@ describe("Modbus client test", () => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:function": 1, }; const result = await client.readResource(form); @@ -215,11 +175,8 @@ describe("Modbus client test", () => { testServer.setRegisters([0, 1, 1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:quantity": 3, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=3", + "modv:function": 1, }; const result = await client.readResource(form); @@ -231,11 +188,8 @@ describe("Modbus client test", () => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 2, - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:function": 2, }; const result = await client.readResource(form); @@ -247,11 +201,8 @@ describe("Modbus client test", () => { testServer.setRegisters([0, 1, 1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 2, - "modbus:address": 0, - "modbus:quantity": 3, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=3", + "modv:function": 2, }; const result = await client.readResource(form); @@ -263,12 +214,9 @@ describe("Modbus client test", () => { testServer.setRegisters([3]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", contentType: "application/octet-stream", - "modbus:function": 3, - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + "modv:function": 3, }; const result = await client.readResource(form); @@ -281,12 +229,9 @@ describe("Modbus client test", () => { testServer.setRegisters([3, 2, 1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=3", contentType: "application/octet-stream; length=6", - "modbus:function": 3, - "modbus:address": 0, - "modbus:quantity": 3, - "modbus:unitID": 1, + "modv:function": 3, }; const result = await client.readResource(form); @@ -299,11 +244,8 @@ describe("Modbus client test", () => { testServer.setRegisters([3]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 4, - "modbus:address": 0, - "modbus:quantity": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=1", + "modv:function": 4, }; const result = await client.readResource(form); @@ -315,11 +257,8 @@ describe("Modbus client test", () => { testServer.setRegisters([3, 2, 1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 4, - "modbus:address": 0, - "modbus:quantity": 3, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=3", + "modv:function": 4, }; const result = await client.readResource(form); @@ -329,11 +268,8 @@ describe("Modbus client test", () => { it("should throw exception for unknown function", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 255, - "modbus:address": 0, - "modbus:quantity": 3, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0?quantity=3", + "modv:function": 255, }; const promise = client.readResource(form); @@ -343,24 +279,32 @@ describe("Modbus client test", () => { it("should throw exception for missing address", () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1", + "modv:function": 1, + }; + + const promise = client.readResource(form); + + return promise.should.eventually.rejectedWith("Malformed href: unitID and address must be defined"); + }); + + it("should throw exception for missing unitID", () => { + const form: ModbusForm = { + href: "modbus+tcp://127.0.0.1:8502", + "modv:function": 1, }; const promise = client.readResource(form); - return promise.should.eventually.rejectedWith("Malformed form: address must be defined"); + return promise.should.eventually.rejectedWith("Malformed href: unitID and address must be defined"); }); }); describe("write resource", () => { it("should write a resource using write coil function", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 5, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 5, }; await client.writeResource(form, new Content("", Readable.from([1]))); @@ -368,10 +312,8 @@ describe("Modbus client test", () => { }); it("should write a resource using multiple write coil function", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 15, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 15, }; await client.writeResource(form, new Content("", Readable.from([1, 0, 1]))); @@ -380,10 +322,8 @@ describe("Modbus client test", () => { it("should write a resource using write register function", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 6, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 6, }; await client.writeResource(form, new Content("", Readable.from([1, 1]))); @@ -392,10 +332,8 @@ describe("Modbus client test", () => { it("should write a resource using write multiple register function", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 16, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 16, }; await client.writeResource(form, new Content("", Readable.from([1, 2, 1, 1]))); @@ -404,11 +342,9 @@ describe("Modbus client test", () => { it("should write a resource with big endian ordering", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - contentType: "application/octet-stream;length=2;byteSeq=BIG_ENDIAN", - "modbus:function": 16, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + contentType: "application/octet-stream;length=2;byteSeq=BIG_ENDIAN", // FIXME: Use mostsignficant etc. + "modv:function": 16, }; await client.writeResource(form, new Content("", Readable.from([0x25, 0x49, 0x59, 0x60]))); @@ -417,11 +353,9 @@ describe("Modbus client test", () => { it("should write a resource with little endian ordering", async () => { const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", + href: "modbus+tcp://127.0.0.1:8502/1/0", contentType: "application/octet-stream;byteSeq=LITTLE_ENDIAN", - "modbus:function": 6, - "modbus:address": 0, - "modbus:unitID": 1, + "modv:function": 6, }; await client.writeResource(form, new Content("", Readable.from([0x01, 0x01]))); // 257 little-endian @@ -433,10 +367,8 @@ describe("Modbus client test", () => { it("should wait for subscription", (done) => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:unitID": 1, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 1, }; client @@ -452,11 +384,9 @@ describe("Modbus client test", () => { it("should poll data", (done) => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:unitID": 1, - "modbus:pollingTime": 250, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 1, + "modv:pollingTime": 250, }; client.subscribeResource(form, async (value) => { @@ -470,11 +400,9 @@ describe("Modbus client test", () => { it("should poll multiple data", (done) => { testServer.setRegisters([1]); const form: ModbusForm = { - href: "modbus://127.0.0.1:8502", - "modbus:function": 1, - "modbus:address": 0, - "modbus:unitID": 1, - "modbus:pollingTime": 125, + href: "modbus+tcp://127.0.0.1:8502/1/0", + "modv:function": 1, + "modv:pollingTime": 125, }; let count = 0; client.subscribeResource(form, async (value) => { diff --git a/packages/binding-modbus/test/modbus-connection-test.ts b/packages/binding-modbus/test/modbus-connection-test.ts index 3f0562b7f..25db80dfb 100644 --- a/packages/binding-modbus/test/modbus-connection-test.ts +++ b/packages/binding-modbus/test/modbus-connection-test.ts @@ -64,14 +64,14 @@ describe("Modbus connection", () => { describe("Operation", () => { it("should fail for unknown host", async () => { 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, + href: "modbus+tcp://127.0.0.2:8502/1/0?quantity=1", + "modv:function": "writeMultipleCoils", + "modv:address": 0, + "modv:quantity": 1, + "modv:unitID": 1, + "modv:entity": "HoldingRegister", + "modv:timeout": 1000, + "modv:pollingTime": 1000, }; const connection = new ModbusConnection("127.0.0.2", 8503, { connectionTimeout: 200, @@ -89,14 +89,14 @@ describe("Modbus connection", () => { it("should throw with timeout", async () => { 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, + href: "modbus+tcp://127.0.0.1:8502/1/4444?quantity=1", + "modv:function": ModbusFunction.readCoil, + "modv:entity": "Coil", + "modv:address": 4444, + "modv:quantity": 1, + "modv:unitID": 1, + "modv:timeout": 1000, + "modv:pollingTime": 1000, }; const connection = new ModbusConnection("127.0.0.1", 8502, { connectionTimeout: 100,