Skip to content

Commit fdc678b

Browse files
authored
Merge pull request #1261 from eclipse-thingweb/ege-modbus
Modbus binding alignment
2 parents 2dc193e + e886d98 commit fdc678b

6 files changed

Lines changed: 306 additions & 305 deletions

File tree

packages/binding-modbus/README.md

Lines changed: 96 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,98 @@
33
## Overview
44

55
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).
6-
This package uses [modbus-serial](https://www.npmjs.com/package/modbus-serial) as a low level client for Modbus TCP.
6+
This package uses [modbus-serial](https://www.npmjs.com/package/modbus-serial) as a low-level client for Modbus TCP.
77
W3C WoT Binding Template for Modbus can be found [here](https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/index.html).
88

99
Current Maintainer(s): [@relu91](https://github.com/relu91) [@fillobotto](https://github.com/fillobotto)
1010

11-
## Protocol specifier
11+
## Client Example
12+
13+
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 <https://github.com/eclipse-thingweb/test-things/tree/main/things/elevator>.
14+
15+
```js
16+
// Protocols and Servient
17+
Servient = require("@node-wot/core").Servient;
18+
ModbusClientFactory = require("@node-wot/binding-modbus").ModbusClientFactory;
19+
20+
// create Servient and add Modbus binding
21+
let servient = new Servient();
22+
servient.addClientFactory(new ModbusClientFactory());
23+
24+
async function main() {
25+
let td = {}; // see https://github.com/eclipse-thingweb/test-things/blob/main/things/elevator/modbus/js/modbus-elevator.td.json
26+
27+
const WoT = await servient.start();
28+
const thing = await WoT.consume(td);
29+
30+
const readData1 = await thing.readProperty("lightSwitch"); // coil
31+
const readData2 = await thing.readProperty("floorNumber"); // register
32+
33+
const readValue1 = await readData1.value();
34+
console.log(readValue1);
35+
36+
const readValue2 = await readData2.value();
37+
console.log(readValue2);
38+
}
39+
40+
main();
41+
```
42+
43+
## Binding Information
44+
45+
### Protocol specifier
1246

1347
The protocol prefix handled by this binding is `modbus+tcp://`.
1448

15-
## New Form Fields for the Modbus Binding
49+
### New Form Fields for the Modbus Binding
1650

1751
**Note**: for further details please refer to the [documentation](https://github.com/eclipse-thingweb/node-wot/blob/master/packages/binding-modbus/src/modbus.ts).
1852

19-
### modbus:function
53+
#### modv:function
54+
55+
Specifies which Modbus function should be issued in the requested command. The list of the available functions is the following:
56+
57+
| Function ID | Label |
58+
| ----------- | :---------------------------: |
59+
| 1 | readCoil |
60+
| 2 | readDiscreteInput |
61+
| 3 | readHoldingRegisters |
62+
| 4 | readInputRegisters |
63+
| 5 | writeSingleCoil |
64+
| 6 | writeSingleHoldingRegister |
65+
| 15 | writeMultipleCoils |
66+
| 16 | writeMultipleHoldingRegisters |
2067

21-
Specifies which modbus function should be issue in the requested command. The list of the available function is the following:
22-
| Function ID | Label
23-
| ------------- |:--------------------:|
24-
| 1 | readCoil |
25-
| 2 | readDiscreteInput |
26-
| 3 | readHoldingRegisters |
27-
| 4 | readInputRegisters |
28-
| 5 | writeSingleCoil |
29-
| 6 | writeSingleHoldingRegister |
30-
| 15 | writeMultipleCoils |
31-
| 16 | writeMultipleHoldingRegisters |
32-
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.
33-
Function IDs or labels can be either used as values of `modbus:function` property.
68+
This list is from [the Modbus Binding Template](https://w3c.github.io/wot-binding-templates/bindings/protocols/modbus/#function).
69+
Function IDs are used on the wire but the labels should be used in a TD as the values of `modv:function` property.
3470

35-
### modbus:entity
71+
#### modv:entity
3672

37-
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]
73+
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].
3874

39-
**Notice** that when used in conjunction with `modbus:function`, the value of `modbus:function` property is ignored.
75+
**Note** that when used in conjunction with `modv:function`, the value of `modv:entity` property is ignored.
4076

41-
### modbus:unitID
77+
#### modv:unitID
4278

43-
The physical bus address of the unit targeted by the modbus request.
79+
The physical bus address of the unit targeted by the Modbus request.
4480

45-
### modbus:address
81+
#### modv:address
4682

4783
This property defines the starting address of registers or coils that are meant to be written.
4884

49-
### modbus:quantity
85+
#### modv:quantity
5086

51-
This property defines the total amount of registers or coils that should be written, beginning with the register specified with the property 'modbus:address'.
87+
This property defines the total amount of registers or coils that should be written, beginning with the register specified with the property `modv:address`.
5288

53-
### modbus:pollingTime
89+
#### modv:pollingTime
5490

55-
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.
91+
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.
5692

57-
### modbus:timeout
93+
#### modv:timeout
5894

59-
Timeout in milliseconds of the modbus request. Default to 1000 milliseconds
95+
Timeout in milliseconds of the Modbus request. Default to 1000 milliseconds
6096

61-
## URL format
97+
### URL format
6298

6399
The URL is used to transport all addressing information necessary to describe the MODBUS connection and register addresses. It has the following structure:
64100

@@ -68,17 +104,17 @@ modbus+tcp:// <host> [ : <port> ] [/ <unitid> [ / <address> ] [&quantity=<quanti
68104

69105
with the following meaning:
70106

71-
- `<host>` is the host name or IP address of the MODBUS slave
107+
- `<host>` is the hostname or IP address of the MODBUS slave
72108
- `<port>` is the optional TCP port number used to access the MODBUS slave. Default is 502
73-
- `<unitid>` is the MODBUS unit id of the MODBUS slave; same as [modbus:unitID](#modbus:unitID)
74-
- `<address>` is the starting address register number; see [modbus:address](#modbus:address)
75-
- `<quantity>` is the optional number of registers to access. Default is 1; see [modbus:quantity](#modbus:quantity)
109+
- `<unitid>` is the MODBUS unit id of the MODBUS slave; same as [modv:unitID](#modv:unitID)
110+
- `<address>` is the starting address register number; see [modv:address](#modv:address)
111+
- `<quantity>` is the optional number of registers to access. Default is 1; see [modv:quantity](#modv:quantity)
76112

77113
When specified URL values override the corresponding `form` parameter.
78114

79-
## DataSchema
115+
### DataSchema
80116

81-
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.
117+
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.
82118

83119
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:
84120

@@ -87,7 +123,7 @@ Along with content type `application/octet-stream`, this protocol binding accept
87123
- `BIG_ENDIAN_BYTE_SWAP`
88124
- `LITTLE_ENDIAN_BYTE_SWAP`
89125

90-
**Note**: the list may be extended in the future.
126+
**Note**: the list above may be extended in the future.
91127

92128
In particular, the decimal numbers `9545` and `22880` will be encoded as follows:
93129

@@ -96,45 +132,45 @@ In particular, the decimal numbers `9545` and `22880` will be encoded as follows
96132
- `BIG_ENDIAN_BYTE_SWAP`: `49 25 60 59`
97133
- `LITTLE_ENDIAN_BYTE_SWAP`: `59 60 25 49`
98134

99-
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!
135+
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.
100136

101137
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
102-
be equal to the number of registers multiplied by the registers size in bytes.
138+
be equal to the number of registers multiplied by the size of the register in bytes.
103139

104-
## Security
140+
### Security
105141

106142
The protocol does not support security.
107143

108-
# Implementation notes
144+
## Implementation notes
109145

110146
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:
111147

112148
- A **request** is a read or write request to a resource as issued by a user of the node-wot API.
113-
- A **transaction** is a MODBUS operation and may cover the data of multiple **requests**.
149+
- A **transaction** is a Modbus operation and may cover the data of multiple **requests**.
114150

115-
## Combination
151+
### Combination
116152

117-
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.
153+
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.
118154

119-
## Serialization
155+
### Serialization
120156

121157
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.
122158

123-
## Combination using the node-wot API
159+
### Combination using the node-wot API
124160

125-
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:
161+
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:
126162

127163
```javascript
128164
console.info("Creating promise vl1n");
129-
let vl1n = pac3200.properties["Voltage/L1N"].read();
165+
let vl1n = pac3200Thing.readProperty("Voltage/L1N");
130166
console.info("Creating promise vl2n");
131-
let vl2n = pac3200.properties["Voltage/L2N"].read();
167+
let vl2n = pac3200Thing.readProperty("Voltage/L2N");
132168
console.info("Creating promise vl3n");
133-
let vl3n = pac3200.properties["Voltage/L3N"].read();
169+
let vl3n = pac3200Thing.readProperty("Voltage/L3N");
134170

135171
console.info("Resolving all promises");
136172
Promise.all([vl1n, vl2n, vl3n])
137-
.then((values) => values.forEach((v) => console.info("Voltage = ", v)))
173+
.then((values) => values.forEach((v) => console.info("Voltage = ", await v.value())))
138174
.catch((reason) => console.warn("Failed ", reason));
139175
```
140176

@@ -148,13 +184,11 @@ Reads the 8th input register of the unit 1
148184

149185
```json
150186
{
151-
"href": "modbus://127.0.0.1:60000",
187+
"href": "modbus+tcp://127.0.0.1:60000/1/8",
152188
"contentType": "application/octet-stream;length=2",
153189
"op": ["readproperty"],
154-
"modbus:function": "readInputRegister",
155-
"modbus:address": 8,
156-
"modbus:unitID": 1,
157-
"modbus:timeout": 2000
190+
"modv:function": "readInputRegister",
191+
"modv:timeout": 2000
158192
}
159193
```
160194

@@ -164,12 +198,10 @@ Read and write the 8th holding register of the unit 1
164198

165199
```json
166200
{
167-
"href": "modbus://127.0.0.1:60000",
201+
"href": "modbus+tcp://127.0.0.1:60000/1/8",
168202
"contentType": "application/octet-stream;length=2",
169203
"op": ["readproperty", "writeproperty"],
170-
"modbus:entity": "HoldingRegister",
171-
"modbus:address": 8,
172-
"modbus:unitID": 1
204+
"modv:entity": "HoldingRegister"
173205
}
174206
```
175207

@@ -179,13 +211,11 @@ Polls the 8th holding register of the unit 1 every second.
179211

180212
```json
181213
{
182-
"href": "modbus://127.0.0.1:60000",
214+
"href": "modbus+tcp://127.0.0.1:60000/1/8",
183215
"contentType": "application/octet-stream;length=2",
184216
"op": ["observeproperty"],
185-
"modbus:entity": "HoldingRegister",
186-
"modbus:address": 8,
187-
"modbus:unitID": 1,
188-
"modbus:pollingTime": 1000
217+
"modv:entity": "HoldingRegister",
218+
"modv:pollingTime": 1000
189219
}
190220
```
191221

@@ -202,4 +232,4 @@ Polls the 8th holding register of the unit 1 every second.
202232

203233
## More Details
204234

205-
See <https://github.com/eclipse-thingweb/node-wot/>
235+
See <https://github.com/eclipse-thingweb/node-wot/>.

0 commit comments

Comments
 (0)