smithtek-mako-rf 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/common-functions.js +190 -0
- package/icon.png +0 -0
- package/icons/icon.png +0 -0
- package/package.json +24 -0
- package/smithtek-mako-rf.html +552 -0
- package/smithtek-mako-rf.js +581 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
**Dedicated PassPort Node that reads and parses the Mako over RF or RS485**
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
**Installation**
|
|
9
|
+
Install using the `NodeRED palette manager`
|
|
10
|
+
|
|
11
|
+
**Smithtek Mako RF**
|
|
12
|
+
RS-485 / LoRa RF Modbus RTU Master for Mako PLC devices.
|
|
13
|
+
|
|
14
|
+
This node combines Modbus polling, retry logic, queue management and structured decoding into a single industrial block.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Bus Configuration
|
|
19
|
+
|
|
20
|
+
Select the communication channel:
|
|
21
|
+
|
|
22
|
+
- **RS485-1** → /dev/ttyAMA0
|
|
23
|
+
- **RS485-2** → /dev/ttyAMA1
|
|
24
|
+
- **RF** → Long range LoRa radio link
|
|
25
|
+
|
|
26
|
+
When RF is selected, serial parameters are automatically managed.
|
|
27
|
+
|
|
28
|
+
Timeout, Retries and Gap are configured in **seconds**.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Mode
|
|
33
|
+
|
|
34
|
+
### Read
|
|
35
|
+
Polls registers from the remote Mako and outputs structured JSON.
|
|
36
|
+
|
|
37
|
+
### Write
|
|
38
|
+
Sends values to the remote device using:
|
|
39
|
+
|
|
40
|
+
- FC5 – Write Single Coil
|
|
41
|
+
- FC6 – Write Single Register
|
|
42
|
+
- FC15 – Write Multiple Coils
|
|
43
|
+
- FC16 – Write Multiple Registers
|
|
44
|
+
|
|
45
|
+
Values can come from `msg.payload` or be fixed in the node configuration.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Device ID
|
|
50
|
+
|
|
51
|
+
The Modbus Unit ID of the remote Mako (1–247).
|
|
52
|
+
Must match the ID configured in the target device.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Function Code
|
|
57
|
+
|
|
58
|
+
Must match the table type configured in your V-NET program:
|
|
59
|
+
|
|
60
|
+
- Read Coils
|
|
61
|
+
- Read Digital Inputs
|
|
62
|
+
- Read Holding Registers
|
|
63
|
+
- Read Input Registers
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Address
|
|
68
|
+
|
|
69
|
+
Starting Modbus register address.
|
|
70
|
+
Must align exactly with the Modbus table built in your Mako V-NET program.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quantity
|
|
75
|
+
|
|
76
|
+
Total number of registers to read.
|
|
77
|
+
|
|
78
|
+
- 16 bit value = 1 register
|
|
79
|
+
- 32 bit value = 2 registers
|
|
80
|
+
|
|
81
|
+
**Example:**
|
|
82
|
+
3 × 32 bit floats = Quantity 6
|
|
83
|
+
|
|
84
|
+
Quantity must cover all registers defined in your decoder map.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Decoder Map
|
|
89
|
+
|
|
90
|
+
Build the register table in the exact same order as your Mako V-NET Modbus table.
|
|
91
|
+
|
|
92
|
+
Each row defines how raw Modbus data is converted into structured JSON.
|
|
93
|
+
|
|
94
|
+
### Name
|
|
95
|
+
Becomes the output key in `msg.payload`.
|
|
96
|
+
Nested paths are supported using: `pump=>status`
|
|
97
|
+
|
|
98
|
+
### Register Data Type
|
|
99
|
+
|
|
100
|
+
- 16 bit integer
|
|
101
|
+
- 16 bit unsigned
|
|
102
|
+
- 32 bit integer
|
|
103
|
+
- 32 bit unsigned
|
|
104
|
+
- 32 bit float
|
|
105
|
+
- Digital to unsigned binary encoder
|
|
106
|
+
|
|
107
|
+
### Offset (regs)
|
|
108
|
+
|
|
109
|
+
Offset is configured in registers.
|
|
110
|
+
|
|
111
|
+
Automatic stepping behaviour:
|
|
112
|
+
|
|
113
|
+
- 16 bit types increase by 1 register
|
|
114
|
+
- 32 bit types increase by 2 registers
|
|
115
|
+
- Digital encoder increases bit number from 0–15 before moving to the next register
|
|
116
|
+
|
|
117
|
+
Offsets must match your V-NET Modbus table layout exactly.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Digital to Unsigned Binary Encoder
|
|
122
|
+
|
|
123
|
+
Used when one 16 bit register contains multiple digital states.
|
|
124
|
+
|
|
125
|
+
The Bit column becomes active.
|
|
126
|
+
Bit automatically increments when adding rows.
|
|
127
|
+
After Bit 15, the next row moves to the next register.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Scale
|
|
132
|
+
|
|
133
|
+
Optional value transformation.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
|
|
137
|
+
- `0.1` → multiply by 0.1
|
|
138
|
+
- `>100`
|
|
139
|
+
- `<50`
|
|
140
|
+
- `==1`
|
|
141
|
+
- `<<2`
|
|
142
|
+
- `>>1`
|
|
143
|
+
|
|
144
|
+
If blank, raw decoded value is used.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Output Behaviour
|
|
149
|
+
|
|
150
|
+
### Successful Read
|
|
151
|
+
|
|
152
|
+
- `msg.payload` → Decoded structured JSON
|
|
153
|
+
- `msg.modbus.raw` → Raw Modbus response
|
|
154
|
+
- `msg.modbus.req` → Request metadata
|
|
155
|
+
|
|
156
|
+
### Successful Write
|
|
157
|
+
|
|
158
|
+
- `msg.payload` → Device confirmation response
|
|
159
|
+
- `msg.modbus.raw` → Raw Modbus confirmation
|
|
160
|
+
|
|
161
|
+
The write response is the actual Modbus confirmation returned by the remote device.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Industrial Behaviour
|
|
166
|
+
|
|
167
|
+
- Single shared bus queue per channel
|
|
168
|
+
- One request processed at a time
|
|
169
|
+
- Timeout handled in seconds
|
|
170
|
+
- Configurable retry attempts
|
|
171
|
+
- Optional inter-request gap
|
|
172
|
+
|
|
173
|
+
Designed for stable Modbus RTU polling over RS485 or long-range RF links.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# License
|
|
178
|
+
Copyright (c) 2023 www.smithtek.com.au Licenced under the terms of the GPLv3
|
|
179
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
180
|
+
AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
181
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
182
|
+
ARE DISCLAIMED. IN NO EVENT SHALL DAMIEN CLARK BE LIABLE FOR ANY DIRECT,
|
|
183
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
184
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
185
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
186
|
+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
187
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
188
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
189
|
+
|
|
190
|
+
www.smithtek.com.au
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* number2bcd -> takes a bcd number and returns the corresponding decimal value
|
|
4
|
+
* @param {Number} number BCD number to convert
|
|
5
|
+
* @param {*} digits no of digits (default 4)
|
|
6
|
+
*/
|
|
7
|
+
const bcd2number = function (number, digits = 4) {
|
|
8
|
+
let loByte = (number & 0x00ff);
|
|
9
|
+
let hiByte = (number >> 8) & 0x00ff;
|
|
10
|
+
let n = 0;
|
|
11
|
+
n += (loByte & 0x0F) * 1;
|
|
12
|
+
if (digits < 2) return n;
|
|
13
|
+
n += ((loByte >> 4) & 0x0F) * 10;
|
|
14
|
+
if (digits < 3) return n;
|
|
15
|
+
n += (hiByte & 0x0F) * 100;
|
|
16
|
+
if (digits < 4) return n;
|
|
17
|
+
n += ((hiByte >> 4) & 0x0F) * 1000;
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* number2bcd -> takes a number and returns the corresponding BCD value.
|
|
24
|
+
* @param {Number} number number to convert to bcd
|
|
25
|
+
* @param {Number} [digits] no of digits (default 4)
|
|
26
|
+
* @returns {Buffer} nodejs buffer
|
|
27
|
+
*/
|
|
28
|
+
const number2bcd = function (number, digits) {
|
|
29
|
+
var s = digits || 4; //default value: 4
|
|
30
|
+
var n = 0;
|
|
31
|
+
|
|
32
|
+
n = (number % 10);
|
|
33
|
+
number = (number / 10) | 0;
|
|
34
|
+
if (s < 2) return n;
|
|
35
|
+
n += (number % 10) << 4;
|
|
36
|
+
number = (number / 10) | 0;
|
|
37
|
+
if (s < 3) return n;
|
|
38
|
+
n += (number % 10) << 8;
|
|
39
|
+
number = (number / 10) | 0;
|
|
40
|
+
if (s < 4) return n;
|
|
41
|
+
n += (number % 10) << 12;
|
|
42
|
+
number = (number / 10) | 0;
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function byteToBits(val) {
|
|
47
|
+
var bits = [];
|
|
48
|
+
for (let index = 0; index < 8; index++) {
|
|
49
|
+
const bit = getBit(val, index);
|
|
50
|
+
bits.push(bit);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
bits: bits,
|
|
55
|
+
bit0: bits[0],
|
|
56
|
+
bit1: bits[1],
|
|
57
|
+
bit2: bits[2],
|
|
58
|
+
bit3: bits[3],
|
|
59
|
+
bit4: bits[4],
|
|
60
|
+
bit5: bits[5],
|
|
61
|
+
bit6: bits[6],
|
|
62
|
+
bit7: bits[7],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function wordToBits(val) {
|
|
66
|
+
var bits = [];
|
|
67
|
+
for (let index = 0; index < 16; index++) {
|
|
68
|
+
const bit = getBit(val, index);
|
|
69
|
+
bits.push(bit);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
bits: bits,
|
|
73
|
+
bit0: bits[0],
|
|
74
|
+
bit1: bits[1],
|
|
75
|
+
bit2: bits[2],
|
|
76
|
+
bit3: bits[3],
|
|
77
|
+
bit4: bits[4],
|
|
78
|
+
bit5: bits[5],
|
|
79
|
+
bit6: bits[6],
|
|
80
|
+
bit7: bits[7],
|
|
81
|
+
bit8: bits[8],
|
|
82
|
+
bit9: bits[9],
|
|
83
|
+
bit10: bits[10],
|
|
84
|
+
bit11: bits[11],
|
|
85
|
+
bit12: bits[12],
|
|
86
|
+
bit13: bits[13],
|
|
87
|
+
bit14: bits[14],
|
|
88
|
+
bit15: bits[15],
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//Get Bit
|
|
93
|
+
function getBit(number, bitPosition) {
|
|
94
|
+
return (number & (1 << bitPosition)) === 0 ? 0 : 1;
|
|
95
|
+
}
|
|
96
|
+
//Set Bit
|
|
97
|
+
function setBit(number, bitPosition) {
|
|
98
|
+
return number | (1 << bitPosition);
|
|
99
|
+
}
|
|
100
|
+
//Clear Bit
|
|
101
|
+
function clearBit(number, bitPosition) {
|
|
102
|
+
const mask = ~(1 << bitPosition);
|
|
103
|
+
return number & mask;
|
|
104
|
+
}
|
|
105
|
+
//Update Bit
|
|
106
|
+
function updateBit(number, bitPosition, bitValue) {
|
|
107
|
+
const bitValueNormalized = bitValue ? 1 : 0;
|
|
108
|
+
const clearMask = ~(1 << bitPosition);
|
|
109
|
+
return (number & clearMask) | (bitValueNormalized << bitPosition);
|
|
110
|
+
}
|
|
111
|
+
function bitsToByte(bits) {
|
|
112
|
+
var byte = 0;
|
|
113
|
+
for (let index = 0; index < 8; index++) {
|
|
114
|
+
let bit = bits[index];
|
|
115
|
+
if (bit) byte = setBit(byte, index);
|
|
116
|
+
}
|
|
117
|
+
return byte;
|
|
118
|
+
}
|
|
119
|
+
function bitsToWord(val) {
|
|
120
|
+
var wd = 0;
|
|
121
|
+
for (let index = 0; index < 16; index++) {
|
|
122
|
+
let bit = val[index];
|
|
123
|
+
if (bit) wd = setBit(wd, index);
|
|
124
|
+
}
|
|
125
|
+
return wd;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SWAPOPTS = ["swap16", "swap32", "swap64"];
|
|
129
|
+
const TYPEOPTS = [
|
|
130
|
+
"int", "int8", "byte",
|
|
131
|
+
"uint", "uint8",
|
|
132
|
+
"int16", "int16le", "int16be", "uint16", "uint16le", "uint16be",
|
|
133
|
+
"int32", "int32le", "int32be", "uint32", "uint32le", "uint32be",
|
|
134
|
+
"bigint64", "bigint64le", "bigint64be", "biguint64", "biguint64le", "biguint64be",
|
|
135
|
+
"float", "floatle", "floatbe", "double", "doublele", "doublebe",
|
|
136
|
+
"8bit", "16bit", "16bitle", "16bitbe", "bool",
|
|
137
|
+
"bcd", "bcdle", "bcdbe",
|
|
138
|
+
"string", "hex", "ascii", "utf8", "utf-8", "utf16le", "ucs2", "latin1", "binary", "buffer"
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* helper function to set a nested property by path
|
|
143
|
+
* @param {*} obj - the object in which to set a properties value
|
|
144
|
+
* @param {string} path - the path to the property e.g. payload.value
|
|
145
|
+
* @param {*} val - the value to set in obj.path
|
|
146
|
+
*/
|
|
147
|
+
function setObjectProperty(obj, path, val, sep) {
|
|
148
|
+
sep = sep == null ? "." : sep;
|
|
149
|
+
const keys = path.split(sep);
|
|
150
|
+
const lastKey = keys.pop();
|
|
151
|
+
const lastObj = keys.reduce((obj, key) =>
|
|
152
|
+
obj[key] = obj[key] || {},
|
|
153
|
+
obj);
|
|
154
|
+
lastObj[lastKey] = val;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* helper function to get a property by path
|
|
159
|
+
* @param {*} obj - the object in which to set a properties value
|
|
160
|
+
* @param {string} path - the path to the property e.g. payload.value
|
|
161
|
+
* @param {*} [sep] - the path property separator (defaults to `.`)
|
|
162
|
+
*/
|
|
163
|
+
function getObjectProperty(obj, path, sep) {
|
|
164
|
+
sep = sep == null ? "." : sep;
|
|
165
|
+
for (var i=0, path=path.split(sep), len=path.length; i<len; i++){
|
|
166
|
+
obj = obj[path[i]];
|
|
167
|
+
};
|
|
168
|
+
return obj;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isNumber(n) {
|
|
172
|
+
if (n === "" || n === true || n === false) return false;
|
|
173
|
+
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
exports.bcd2number = bcd2number;
|
|
177
|
+
exports.number2bcd = number2bcd;
|
|
178
|
+
exports.byteToBits = byteToBits;
|
|
179
|
+
exports.wordToBits = wordToBits;
|
|
180
|
+
exports.bitsToByte = bitsToByte;
|
|
181
|
+
exports.bitsToWord = bitsToWord;
|
|
182
|
+
exports.getBit = getBit;
|
|
183
|
+
exports.setBit = setBit;
|
|
184
|
+
exports.clearBit = clearBit;
|
|
185
|
+
exports.updateBit = updateBit;
|
|
186
|
+
exports.isNumber = isNumber;
|
|
187
|
+
exports.setObjectProperty = setObjectProperty;
|
|
188
|
+
exports.getObjectProperty = getObjectProperty;
|
|
189
|
+
exports.SWAPOPTS = SWAPOPTS;
|
|
190
|
+
exports.TYPEOPTS = TYPEOPTS;
|
package/icon.png
ADDED
|
Binary file
|
package/icons/icon.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smithtek-mako-rf",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Smithtek Mako RF: Modbus RTU (RS-485) over RF with shared bus queue, timeout seconds and retries.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"smithtek",
|
|
8
|
+
"modbus",
|
|
9
|
+
"rtu",
|
|
10
|
+
"rs485",
|
|
11
|
+
"lora"
|
|
12
|
+
],
|
|
13
|
+
"license": "GPL-3.0-only",
|
|
14
|
+
"author": "Smithtek",
|
|
15
|
+
"homepage": "https://www.smithtek.com.au",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"modbus-serial": "^9.0.10"
|
|
18
|
+
},
|
|
19
|
+
"node-red": {
|
|
20
|
+
"nodes": {
|
|
21
|
+
"smithtek-mako-rf": "smithtek-mako-rf.js"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|