Open volkmarnissen opened 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
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.
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:
This is the debug output