yaacov / node-modbus-serial

A pure JavaScript implemetation of MODBUS-RTU (and TCP) for NodeJS
ISC License
644 stars 242 forks source link

multiple modbusRTU open/close will end with "Error Resource temporarily unavailable Cannot lock port" #547

Open volkmarnissen opened 9 months ago

volkmarnissen commented 9 months ago

When you execute ModbusRTU.open/ModbusRTU.close multiple times, it will work only for the first open call. The second open call will fail with "Error Resource temporarily unavailable Cannot lock port".

You can see in the debug log, that second open call does not trigger "Creating poller". May be this is the reason.

As a work around, you can always create a new ModbusRTU and call connectRTU instead of calling ModbusRTU.open.

Here is how to reproduce it:

import Debug from "debug"

const debug = Debug("modbusreopen");

Debug.enable('serialport* modbusreopen')
const client = new ModbusRTU();
connect();
console.log("exit")
let count = 0;

function open() {
    debug("open")
    if (count++ < 4)
        client.open(close)
}

// open connection to a serial port
function close() {
    debug("close")
    if (client.isOpen)
        client.close(open)
    else
        setTimeout(open, 100)
}
function connect() {
    client.connectRTU("/dev/ttyUSB0", { baudRate: 9600 }).then(close).catch((e) => {
        debug("connected")
        console.log(e)
    });
}

This is the debug output

  serialport/stream opening path: /dev/ttyUSB0 +0ms
  serialport/bindings-cpp open +0ms
exit
  serialport/bindings-cpp/poller Creating poller +0ms
  serialport/stream opened path: /dev/ttyUSB0 +3ms
  modbusreopen close +0ms
  serialport/stream #close +1ms
  serialport/bindings-cpp close +3ms
  serialport/bindings-cpp/poller Stopping poller +1ms
  serialport/bindings-cpp/poller Destroying poller +0ms
  serialport/stream _read queueing _read for after open +0ms
  serialport/stream binding.close finished +6ms
  modbusreopen open +7ms
  serialport/stream opening path: /dev/ttyUSB0 +1ms
  serialport/bindings-cpp open +7ms
  serialport/bindings-cpp/poller Creating poller +7ms
  serialport/stream opened path: /dev/ttyUSB0 +0ms
  serialport/stream _read reading { start: 0, toRead: 65536 } +0ms
  serialport/bindings-cpp read +1ms
  serialport/bindings-cpp/unixRead Starting read +0ms
  modbusreopen close +1ms
  serialport/stream #close +2ms
  serialport/bindings-cpp close +1ms
  serialport/bindings-cpp/poller Stopping poller +2ms
  serialport/bindings-cpp/poller Destroying poller +0ms
  serialport/stream binding.close finished +0ms
  modbusreopen open +1ms
  serialport/stream opening path: /dev/ttyUSB0 +0ms
  serialport/bindings-cpp open +0ms
  serialport/stream Binding #open had an error [Error: Error Resource temporarily unavailable Cannot lock port] +0ms
  modbusreopen close +0ms
  modbusreopen open +101ms
  serialport/stream opening path: /dev/ttyUSB0 +101ms
  serialport/bindings-cpp open +101ms
  serialport/stream Binding #open had an error [Error: Error Resource temporarily unavailable Cannot lock port] +1ms
  modbusreopen close +1ms
  modbusreopen open +101ms
  serialport/stream opening path: /dev/ttyUSB0 +101ms
  serialport/bindings-cpp open +102ms
  serialport/stream Binding #open had an error [Error: Error Resource temporarily unavailable Cannot lock port] +1ms
  modbusreopen close +1ms
  modbusreopen open +101ms
volkmarnissen commented 9 months ago

I just found out, that the work around is not really working.

Whenever a call to readHoldingRegister terminates with ETIMEDOUT and you close the connection afterwards, the resource is still locked. If you do a successful readHoldingRegister after ETIMEDOUT Exception, you can close it without locking the resource. So, The ETIMEDOUT exception doesn't unlock the port properly.

import ModbusRTU from 'modbus-serial'
import Debug from "debug"
import { ReadRegisterResult } from 'modbus-serial/ModbusRTU';

const debug = Debug("modbusreopen");

Debug.enable('modbusreopen')
const client = new ModbusRTU();
let count = 0;
connect()
function open() {
    debug("closed " + (client.isOpen ? "is open" : "is closed"))
    if (count++ < 4) {
        debug("open")
        client.connectRTU("/dev/ttyUSB0", { baudRate: 9600 }).then(read).catch((e) => {
            debug("Error connected" + e)
        });
    }
    else {
        debug("exit()");
        process.exit()
    }
}
function read() {
    debug("opened " + (client.isOpen ? "is open" : "is closed"))
    client.setID(7)
    client.setTimeout(100)
    debug("reading")
    client.readHoldingRegisters(1, 1).then(close).catch((e) => {
        debug("failed to read " + JSON.stringify(e))
        if (e.errno && e.errno == "ETIMEDOUT") {
            client.setID(2)
            debug("read from existing device " + count)
            if (count == 0)
                client.readHoldingRegisters(1, 1).then((r) => {
                    debug("read result: " + JSON.stringify(r))
                    close()
                }).catch((e) => {
                    debug("failed to read from existing device" + JSON.stringify(e))
                    close()
                })
            else {
                debug("No successful read after timeout")
                close()
            }
        }
    })
}
// open connection to a serial port
function close(result?: ReadRegisterResult) {
    if (result)
        debug(JSON.stringify(result))
    debug("will close " + (client.isOpen ? "is open" : "is closed"))

    if (client.isOpen) {
        debug("call close")
        client.close(open)
    }
    else {
        debug("read again")
        open()
        //setTimeout(open, 10)
    }

}
function connect() {

    client.connectRTU("/dev/ttyUSB0", { baudRate: 9600 }).then(read).catch((e) => {
        debug("Error connected")
        console.log(e)
    });
}

this produces this output:

  modbusreopen opened is open +0ms
  modbusreopen reading +1ms
  modbusreopen failed to read {"name":"TransactionTimedOutError","message":"Timed out","errno":"ETIMEDOUT"} +101ms
  modbusreopen read from existing device 0 +1ms
  modbusreopen read result: {"data":[1],"buffer":{"type":"Buffer","data":[0,1]}} +41ms
  modbusreopen will close is open +0ms
  modbusreopen call close +0ms
  modbusreopen closed is closed +8ms
  modbusreopen open +0ms
  modbusreopen opened is open +2ms
  modbusreopen reading +0ms
  modbusreopen failed to read {"name":"TransactionTimedOutError","message":"Timed out","errno":"ETIMEDOUT"} +100ms
  modbusreopen read from existing device 1 +1ms
  modbusreopen No successful read after timeout +0ms
  modbusreopen will close is open +0ms
  modbusreopen call close +0ms
  modbusreopen closed is closed +0ms
  modbusreopen open +0ms
  modbusreopen Error connectedError: Error Resource temporarily unavailable Cannot lock port +2ms
teddy1565 commented 7 months ago

I'm investigating something recently, and it happens to be somewhat related to this matter.

maybe you can try client.destroy()

because in this repo implement, distroy will call _cancelPendingTransactions

but close doesn't.

internal variable _transactions maybe lock some resource, like if physical RS485 wiring error promise will never end

until clear _transactions, and resolve promise

when you use client.open or client.readHoldingRegisters, anyway. finally they will call client.open

and client.open will change or depend _transactions status

What I want to express is that it is very likely that the resource operation cannot be responded to until the transaction is completely processed.

but at the same time, I also feel that there are some problems and loopholes in this mechanism.

open source code

open(callback) {
        const modbus = this;

        // open the serial port
        modbus._port.open(function(error) {
            if (error) {
                modbusSerialDebug({ action: "port open error", error: error });
                /* On serial port open error call next function */
                if (callback)
                    callback(error);
            } else {
                /* init ports transaction id and counter */
                modbus._port._transactionIdRead = 1;
                modbus._port._transactionIdWrite = 1;

                /* On serial port success
                 * (re-)register the modbus parser functions
                 */
                modbus._port.removeListener("data", modbus._onReceive);
                modbus._port.on("data", modbus._onReceive);

                /* On serial port error
                 * (re-)register the error listener function
                 */
                modbus._port.removeListener("error", modbus._onError);
                modbus._port.on("error", modbus._onError);

                /* Hook the close event so we can relay it to our callers. */
                modbus._port.once("close", modbus.emit.bind(modbus, "close"));

                /* On serial port open OK call next function with no error */
                if (callback)
                    callback(error);
            }
        });
    }

_onReceive

function _onReceive(data) {
    const modbus = this;
    let error;

    // set locale helpers variables
    const transaction = modbus._transactions[modbus._port._transactionIdRead];

    // the _transactionIdRead can be missing, ignore wrong transaction it's
    if (!transaction) {
        return;
    }

    if (transaction.responses) {
        /* Stash what we received */
        transaction.responses.push(Uint8Array.prototype.slice.call(data));
    }
// ............
volkmarnissen commented 7 months ago

Hi,

destroy doesn't change the situation:

This seems to be a bug.

You can use the test program to reproduce it. Just connect an RS485 USB stick to /dev/ttyUSB0. There is no further hardware connection required.

As I wrote before: Workaround: Do not close or destroy after timeout. The next successful call will clean up. Then you can close the connection.

My ides was: If I close the connection after using it, other apps could also connect to the same USB rs485. This should be a more or less common use case.

import ModbusRTU from 'modbus-serial'
import Debug from "debug"
let baudrate: number = 9600
const debug = Debug("modbusanalysis");
process.env["DEBUG"] = "modbusanalysis"
Debug.enable('modbusanalysis')

let client = new ModbusRTU();
connect();
console.log("exit")

// open connection to a serial port
function destroy() {
    debug("destroy")
    client.destroy(connect)
}
function connect() {
    debug("connect " + baudrate)
    client = new ModbusRTU();
    client.connectRTU("/dev/ttyUSB0", { baudRate: baudrate }).then(read).catch((e) => {
        debug("connect " + JSON.stringify(e))
        console.log(e)
        process.exit(0)
    })
}

function read() {
    debug("read: ")
    client.setID(1)
    client.setTimeout(500)
    client.readHoldingRegisters(10, 1)
        .then(() => {
            debug("success")
            destroy()
        }).catch(e => {
            if (e.errno && e.errno == "ETIMEDOUT")
                debug("timeout")
            else
                debug("read failure: " + JSON.stringify(e))
            destroy();
        });
}
  modbusanalysis connect 9600 +0ms
exit
  modbusanalysis read:  +2ms
  modbusanalysis timeout +502ms
  modbusanalysis destroy +0ms
node:events:510
    throw err; // Unhandled 'error' event
    ^

Error [ERR_UNHANDLED_ERROR]: Unhandled error. (SerialPortError {
  name: 'SerialPortError',
  message: undefined,
  errno: 'ECONNREFUSED',
  stack: undefined
})
    at ModbusRTU.emit (node:events:508:17)
    at ModbusRTU._onError (/home/volkmar/modbus2mqtt/node_modules/modbus-serial/index.js:552:10)
    at SerialPort.emit (node:events:519:28)
    at emitErrorNT (node:internal/streams/destroy:169:8)
    at emitErrorCloseNT (node:internal/streams/destroy:128:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'ERR_UNHANDLED_ERROR',
  context: SerialPortError {
    name: 'SerialPortError',
    message: undefined,
    errno: 'ECONNREFUSED',
    stack: undefined
  }
}
Node.js v21.6.1
teddy1565 commented 7 months ago

I'll give it a try when I have more free time.

volkmarnissen commented 7 months ago

Thank you. Take your time. The workaround is working fine.