node-modbus-serial
node-modbus-serial copied to clipboard
multiple modbusRTU open/close will end with "Error Resource temporarily unavailable Cannot lock port"
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
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
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));
}
// ............
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
I'll give it a try when I have more free time.
Thank you. Take your time. The workaround is working fine.