node-modbus / stream

NodeJS Modbus Stream
MIT License
171 stars 51 forks source link

Syntax for generic `reply` function #38

Closed jacobq closed 3 years ago

jacobq commented 5 years ago

Similar to https://github.com/node-modbus/stream/issues/34 I'm confused about the different signatures that need to be used for reply. It seems to depend on the function code, that is, reply seems to assume the data it receives is byte-for-byte identical to the data section of the response frame, even if that includes additional fields like start address, number of elements to read/write, etc. Is that right?

For example, in my application I define a "modbus map" to associate handler functions with ROB & ROR read operations (function code 2 & 4) as well as RWB & RWR read/write operations (codes 1, 3, 5, 6, 15, and 16). However, I am not certain how I should call reply with the resulting data.

modbus.tcp.server(serverOptions, (connection: any) => {
    // ...
    connection.transport.on('request', (eventName, transaction, reply) => {
        const request = transaction.request;
        const size = request.quantity;
        const firstAddr = request.address;
        const lastAddr = firstAddr + size - 1;
        // (call appropriate handlers)
        // ...
        // This is the part that seems weird / opaque to me.
        // Why isn't there a function that will just accept `result` and format it appropriately
        // (since the transaction contains the information needed)?
        switch(eventName) {
            case 'read-coils':               // fcode 1: response format = [ID][FC][BC][DATA, 1 byte per 8 coils]
            case 'read-discrete-inputs':     // fcode 2: response format = [ID][FC][BC][DATA, 1 byte per 8 coils]
                reply(null, result);
                break;
            case 'read-holding-registers':   // fcode 3: response format = [ID][FC][BC][DATA, 2 bytes per register]
            case 'read-input-registers':     // fcode 4: response format = [ID][FC][BC][DATA, 2 bytes per register]
                result = result.map(x => (x instanceof Buffer) ? x : createUInt16BufferFromNumber(x));
                reply(null, result);
                break;
            case 'write-single-coil':        // fcode 5: response format = [ID][FC][ADDR][DATA] (mirrors request exactly)
            case 'write-single-register':    // fcode 6: response format = [ID][FC][ADDR][DATA] (mirrors request exactly)
                reply(null, firstAddr, request.value);
                break;
            case 'write-multiple-coils':     // fcode 15: response format = [ID][FC][ADDR][NUM]
            case 'write-multiple-registers': // fcode 16: response format = [ID][FC][ADDR][NUM]
                reply(null, firstAddr, size);
                break;
            default:
                warn(`Replying with default format -- probably wrong!`, result);
                reply(null, result);
        }
    })
    // ...
}
dresende commented 5 years ago

Yes, the reply assumes a first (fixed) argument for an error, and then the remaining arguments depend on the function code and what the standard defines for a response.

jacobq commented 5 years ago

Yes, the reply assumes a first (fixed) argument for an error, and then the remaining arguments depend on the function code and what the standard defines for a response.

OK, thank you for clarifying that. Is it the same regardless of whether reply is obtained from a specific event like read-coils vs. from the lower level request event?

connection.on("request", (request, reply) => {
    // ...
});
connection.on("read-coils", (request, reply) => {
    // ...
    // does this `reply` have the same signature/contract as the one above?
});
dresende commented 5 years ago

Is it the same regardless of whether reply is obtained from a specific event like read-coils vs. from the lower level request event?

I believe yes. But perhaps, in that case, it makes more sense to be able to send a raw response. You could just ignore the reply and write a response yourself, but it would be easier.

jacobq commented 5 years ago

OK, thanks for the reply. I will plan to just keep using the request event and it's reply function as I have it (like above -- seems to be working OK). It just feels a little non-intuitive because the library code does some of the protocol work (e.g. ID & function code / exception code) but not all of it (e.g. start address & number of coils/registers), so I have to dig into the library code to see what I need to do. For example, should I be sending "one big buffer" or separate arguments for, say, start address and array of data.

You could just ignore the reply and write a response yourself

Would that look something like this: transport.write(<Buffer>)? (Where <Buffer> holds the "Protocol Data Unit (PDU)")

dresende commented 5 years ago

Yes.

jacobq commented 3 years ago

Closing due to inactivity