ServerTCP: readHoldingRegisters throws Modbus exception 4: Slave device failure
If you implement getMultipleHoldingRegisters for ServerTCP with promise, it'll throw an exception(Modbus exception 4: Slave device failure), when used.
Workaround: Use callback implementation
const vector: IServiceVector = {
...
// This will fail when used
getMultipleHoldingRegisters: (addr: number, length: number, unitID: number): Promise<number[]> => {
debug("getMultipleHoldingRegisters")
return new Promise<number[]>((resolve, reject) => {
let rc: number[] = []
for (let idx = 0; idx < length; idx++) {
let v = values.holdingRegisters.find(v => v.slaveid == unitID && v.address == addr + idx)
if (v)
rc.push(v.value)
else
reject({ modbusErrorCode: 2, msg: "" })
}
resolve(rc)
})
},
...
}
// This will work
getMultipleHoldingRegisters: (addr: number, length: number, unitID: number, cb: FCallbackVal<number[]>): void => {
debug("getMultipleHoldingRegisters")
let rc: number[] = []
for (let idx = 0; idx < length; idx++) {
let v = values.holdingRegisters.find(v => v.slaveid == unitID && v.address == addr + idx)
if (v)
rc.push(v.value)
else {
cb({ modbusErrorCode: 2 } as any as Error, []);
return
}
}
cb(null, rc)
},
Thank you for the issue
Working workaround: If a ETIMEDOUT error happens, don't close the connection. Close only after other errors or successful reads.
I think it's may not a bug?
If you mean server side
In Document
const vector = {
getInputRegister: function(addr, unitID) {
// Synchronous handling
return addr;
},
getHoldingRegister: function(addr, unitID, callback) {
// Asynchronous handling (with callback)
setTimeout(function() {
// callback = function(err, value)
callback(null, addr + 8000);
}, 10);
},
getCoil: function(addr, unitID) {
// Asynchronous handling (with Promises, async/await supported)
return new Promise(function(resolve) {
setTimeout(function() {
resolve((addr % 2) === 0);
}, 10);
});
},
setRegister: function(addr, value, unitID) {
// Asynchronous handling supported also here
console.log("set register", addr, value, unitID);
return;
},
setCoil: function(addr, value, unitID) {
// Asynchronous handling supported also here
console.log("set coil", addr, value, unitID);
return;
},
readDeviceIdentification: function(addr) {
return {
0x00: "MyVendorName",
0x01: "MyProductCode",
0x02: "MyMajorMinorRevision",
0x05: "MyModelName",
0x97: "MyExtendedObject1",
0xAB: "MyExtendedObject2"
};
}
};
That's mean getHoldingRegister can return value or undefined and Document 'Asynchronous' not real mean you can return promise or async function p.s. promise not always equal async
It's mean, when you want like async, you can schedule a task into event-loop on next tick, but in current lifecycle, it should be value or undefined.
But i think there are also other potential problems in this example. like: memory leak or promise blocking (when you emit a promise, no any method can cancel it)
But if you mean client side
when you use getHoldingRegister with multi
server side just need getHoldingRegister vector because, they will get different register value like
for example you get float value from register 0 length 4
reg00: 65 reg01: 14 reg02: 21 reg03: 99
then use IEEE754 like (Buffer.from([65,14,21,99])).readFloatBE(0)
you get the float value
Thank you for the analysis. I was refering to server side.
You are right, it is not a bug.
When I implemented the Server, I was searching for a documentation to implement it in typescript. I didn't found anything. Especially the error handling was not obvious to me.
The promise implementation was compilable in type script, but did not work. I used it, because I was hoping it deals with errors properly.
Meanwhile my typescript implementation is working properly even with error handling It's just a fake server for a few Modbus RTC devices. I attached the code in case someone is interested in it.
I will close the issue, because it's not a bug and now, I have all the information I needed.
Thank you
import { FCallbackVal, IServiceVector, ServerTCP } from "modbus-serial";
import Debug from "debug"
export const XYslaveid = 1
export const Dimplexslaveid = 2
export const Eastronslaveid = 3
const debug = Debug("modbusserver");
const dimplexHolding = [[1,200],
[1,200],
[174,450],
[11,208],
[3,480],
[46,209],
[47,30],
]
const values = {
//XY-MD02
inputRegisters: [{ slaveid: XYslaveid, address: 1, value: 195 }, { slaveid: XYslaveid, address: 2, value: 500},
],
holdingRegisters: [{ slaveid: XYslaveid, address: 0x0101, value: 1 }, { slaveid: XYslaveid, address: 0x0102, value: 1 }],
coils: [{ slaveid: XYslaveid, address: 1, value: true }, { slaveid: XYslaveid, address: 2, value: true },
{ slaveid: Dimplexslaveid, address: 1, value: false },{ slaveid: Dimplexslaveid, address: 3, value: false }],
}
function getCoil(addr: number, unitID: number): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
let v = values.coils.find(v => v.slaveid == unitID && v.address == addr)
if (v){
debug("getCoil: slave: " + unitID + "address: " + addr + "v: " + v.value)
resolve(v.value)
}
else{
debug("getCoil: failed slave: " + unitID + "address: " + addr)
reject({ modbusErrorCode: 2, msg: "" })
}
})
}
const vector: IServiceVector = {
getInputRegister: function (addr: number, unitID: number): Promise<number> {
return new Promise<number>((resolve, reject) => {
let v = values.inputRegisters.find(v => v.slaveid == unitID && v.address == addr)
if (v){
debug("getInputRegister slave:" + addr + "unit" + unitID + "v: " + v.value)
resolve(v.value)
}
else{
debug("getInputRegister slave:" + addr + "unit" + unitID )
reject({ modbusErrorCode: 2, msg: "" })
}
});
},
getHoldingRegister: function (addr: number, unitID: number): Promise<number> {
return new Promise<number>((resolve, reject) => {
let v = values.holdingRegisters.find(v => v.slaveid == unitID && v.address == addr)
if (v){
debug("getHoldingRegister addr:" + addr + " slave: " + unitID + "v: " + v.value)
resolve(v.value)
}
else{
debug("getHoldingRegister not found addr:" + addr + " slave: " + unitID)
reject({ modbusErrorCode: 2, msg: "" })
}
});
},
getMultipleInputRegisters: (addr: number, length: number, unitID: number, cb: FCallbackVal<number[]>): void => {
let rc: number[] = []
for (let idx = 0; idx < length; idx++) {
let v = values.inputRegisters.find(v => v.slaveid == unitID && v.address == addr + idx)
if (v)
rc.push(v.value)
else {
debug("getMultipleInputRegisters not found addr:" + addr + " slave: " + unitID )
cb({ modbusErrorCode: 2 } as any as Error, []);
return
}
}
debug("getMultipleInputRegisters addr:" + addr + " slave: " + unitID + "rc: " + JSON.stringify(rc))
cb(null, rc)
},
getMultipleHoldingRegisters: (addr: number, length: number, unitID: number, cb: FCallbackVal<number[]>): void => {
let rc: number[] = []
for (let idx = 0; idx < length; idx++) {
let v = values.holdingRegisters.find(v => v.slaveid == unitID && v.address == addr + idx)
if (v)
rc.push(v.value)
else {
cb({ modbusErrorCode: 2 } as any as Error, []);
return
}
}
debug("getMultipleHoldingRegisters " + JSON.stringify(rc))
cb(null, rc)
},
getDiscreteInput: getCoil,
getCoil: getCoil,
setRegister: (addr: number, value: number, unitID: number): Promise<void> => {
return new Promise<void>((resolve, reject) => {
let v = values.holdingRegisters.find(v => v.slaveid == unitID && v.address == addr)
if (v) {
v.value = value
resolve()
}
else
reject({ modbusErrorCode: 2, msg: "" })
});
},
setCoil: (addr: number, value: boolean, unitID: number, cb: FCallbackVal<number>): void => {
let v = values.coils.find(v => v.slaveid == unitID && v.address == addr)
if (v) {
v.value = value
cb(null, value ? 1 : 0)
}
else {
cb({ modbusErrorCode: 2 } as any as Error, 0);
return
}
}
};
export class ModbusServer {
serverTCP: ServerTCP | undefined
async startServer(): Promise<ServerTCP> {
dimplexHolding.forEach( (nv)=>{
values.holdingRegisters.push({ slaveid: Dimplexslaveid, address: nv[0], value: nv[1] })
})
let rc = new Promise<ServerTCP>((resolve) => {
console.log("ModbusTCP listening on modbus://0.0.0.0:8502");
this.serverTCP = new ServerTCP(vector, { host: "0.0.0.0", port: 8502, debug: true });
this.serverTCP.on("socketError", function (err) {
// Handle socket error if needed, can be ignored
console.error(err);
});
this.serverTCP.on("initialized", () => { resolve(this.serverTCP!) })
})
return rc;
}
stopServer(cb?: () => void) {
if (this.serverTCP)
this.serverTCP.close(() => {
if (cb)
cb();
})
}
}
export function runModbusServer(): void {
new ModbusServer().startServer().then(() => { console.log("listening") });
}
// set the server to answer for modbus requests