node-modbus-serial icon indicating copy to clipboard operation
node-modbus-serial copied to clipboard

ServerTCP: readHoldingRegisters throws Modbus exception 4: Slave device failure

Open volkmarnissen opened this issue 1 year ago • 1 comments

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)
    },

volkmarnissen avatar Feb 27 '24 14:02 volkmarnissen

Thank you for the issue

yaacov avatar Feb 27 '24 15:02 yaacov

Working workaround: If a ETIMEDOUT error happens, don't close the connection. Close only after other errors or successful reads.

volkmarnissen avatar Mar 04 '24 13:03 volkmarnissen

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)

teddy1565 avatar Apr 09 '24 07:04 teddy1565

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

teddy1565 avatar Apr 09 '24 07:04 teddy1565

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

volkmarnissen avatar Apr 09 '24 08:04 volkmarnissen