Skip to content

Commit

Permalink
Allow to pass socket options to TCP client (#519)
Browse files Browse the repository at this point in the history
* add jsdocs

* allow to pass more opts to socket

* allow to pass AbortSignal to socket

* add abort signal example

* lint the code

* remove vscode/ from gitignore

* drop node v14 (reached EOL April 2023)
  • Loading branch information
AndreMaz authored Oct 19, 2023
1 parent b57f338 commit 9a74d61
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 2017
"ecmaVersion": 2020
},
"extends": "eslint:recommended",
"rules": {
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: ci

on:
on:
push:
branches:
- master
Expand All @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 18.x, 20.x]
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ lib-cov

# Coverage directory used by tools like istanbul
coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
Expand All @@ -33,6 +34,3 @@ modbus-serial
docs

package-lock\.json

# Visual Studio Code
.vscode/
26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current File",
"type": "node",
"request": "launch",
"program": "${file}"
},
{
"type": "node",
"request": "launch",
"name": "Mocha single test",
"program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
// current file
"args": ["${file}"],
"console": "internalConsole",
"cwd": "${workspaceRoot}",
"runtimeVersion": "16",
}
]
}

6 changes: 4 additions & 2 deletions ModbusRTU.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Socket } from 'net';
import { Socket, SocketConstructorOpts, TcpSocketConnectOpts } from 'net';
import { TestPort } from "./TestPort";
import { PortInfo } from '@serialport/bindings-cpp';

Expand Down Expand Up @@ -133,12 +133,14 @@ export interface SerialPortUnixPlatformOptions {
vtime?: number;
}

export interface TcpPortOptions {
export interface TcpPortOptions extends TcpSocketConnectOpts {
port?: number;
localAddress?: string;
family?: number;
ip?: string;
timeout?: number;
socket: Socket;
socketOpts: SocketConstructorOpts
}

export interface UdpPortOptions {
Expand Down
58 changes: 58 additions & 0 deletions examples/tcp_client_abort_signal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use strict";

// create an empty modbus client
// let ModbusRTU = require("modbus-serial");
const ModbusRTU = require("../index");
const client = new ModbusRTU();

const abortController = new AbortController();
const { signal } = abortController;
signal.addEventListener("abort", () => {
console.log("Abort signal received by the abort controller");
});

async function connect() {
await client.connectTCP("127.0.0.1", {
port: 8502,
socketOpts: {
signal: signal
}
});
client.setID(1);
client.setTimeout(2000);
}

async function readRegisters() {
const data = await client.readHoldingRegisters(5, 4);
console.log("Received:", data.data);
}

async function runner() {
await connect();

setTimeout(() => {
abortController.abort("Aborting request");
}, 1000);

await readRegisters();
}

runner()
.then(() => {
if (signal.aborted) {
if (signal.reason) {
console.log(`Request aborted with reason: ${signal.reason}`);
} else {
console.log("Request aborted but no reason was given.");
}
} else {
console.log("Request not aborted");
}
})
.catch((error) => {
console.error(error);
})
.finally(async() => {
console.log("Close client");
await client.close();
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "A pure JavaScript implemetation of MODBUS-RTU (Serial and TCP) for NodeJS.",
"main": "index.js",
"scripts": {
"test": "mocha --recursive"
"test": "nyc --reporter=lcov --reporter=text mocha --recursive"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -34,6 +34,7 @@
"mocha": "^10.2.0",
"mocha-eslint": "^7.0.0",
"mockery": "^2.1.0",
"nyc": "^15.1.0",
"pump": "^3.0.0",
"sinon": "^15.2.0",
"web-bluetooth-mock": "^1.2.0",
Expand Down
81 changes: 55 additions & 26 deletions ports/tcpport.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,59 @@ class TcpPort extends EventEmitter {
/**
* Simulate a modbus-RTU port using modbus-TCP connection.
*
* @param ip
* @param options
* @param {string} ip - IP address of Modbus slave.
* @param {{
* port?: number,
* localAddress?: string,
* family?: 0|4|6,
* timeout?: number,
* socket?: net.Socket
* socketOpts?: {
* fd: number,
* allowHalfOpen?: boolean,
* readable?: boolean,
* writable?: boolean,
* signal?: AbortSignal
* },
* } & net.TcpSocketConnectOpts} options - Options object.
* options.port: Nonstandard Modbus port (default is 502).
* options.localAddress: Local IP address to bind to, default is any.
* options.family: 4 = IPv4-only, 6 = IPv6-only, 0 = either (default).
* @constructor
*/
constructor(ip, options) {
super();
const modbus = this;
const self = this;
/** @type {boolean} Flag to indicate if port is open */
this.openFlag = false;
/** @type {(err?: Error) => void} */
this.callback = null;
this._transactionIdWrite = 1;
/** @type {net.Socket?} - Optional custom socket */
this._externalSocket = null;

if(typeof ip === "object") {
options = ip;
ip = undefined;
}

if (typeof(options) === "undefined") options = {};
if (typeof options === "undefined") options = {};

this.connectOptions = {
host: ip || options.ip,
port: options.port || MODBUS_PORT,
localAddress: options.localAddress,
family: options.family
this.socketOpts = undefined;
if (options.socketOpts) {
this.socketOpts = options.socketOpts;
delete options.socketOpts;
}

/** @type {net.TcpSocketConnectOpts} - Options for net.connect(). */
this.connectOptions = {
// Default options
...{
host: ip || options.ip,
port: MODBUS_PORT
},
// User options
...options
};

if(options.socket) {
Expand All @@ -55,18 +82,20 @@ class TcpPort extends EventEmitter {
}

// handle callback - call a callback function only once, for the first event
// it will triger
// it will trigger
const handleCallback = function(had_error) {
if (modbus.callback) {
modbus.callback(had_error);
modbus.callback = null;
if (self.callback) {
self.callback(had_error);
self.callback = null;
}
};

// init a socket
this._client = this._externalSocket || new net.Socket();
this._client = this._externalSocket || new net.Socket(this.socketOpts);

if (options.timeout) this._client.setTimeout(options.timeout);

// register events handlers
this._client.on("data", function(data) {
let buffer;
let crc;
Expand All @@ -89,34 +118,34 @@ class TcpPort extends EventEmitter {
buffer.writeUInt16LE(crc, buffer.length - CRC_LENGTH);

// update transaction id and emit data
modbus._transactionIdRead = data.readUInt16BE(0);
modbus.emit("data", buffer);
self._transactionIdRead = data.readUInt16BE(0);
self.emit("data", buffer);

// debug
modbusSerialDebug({ action: "parsed tcp port", buffer: buffer, transactionId: modbus._transactionIdRead });
modbusSerialDebug({ action: "parsed tcp port", buffer: buffer, transactionId: self._transactionIdRead });

// reset data
data = data.slice(length + MIN_MBAP_LENGTH);
}
});

this._client.on("connect", function() {
modbus.openFlag = true;
self.openFlag = true;
modbusSerialDebug("TCP port: signal connect");
handleCallback();
});

this._client.on("close", function(had_error) {
modbus.openFlag = false;
self.openFlag = false;
modbusSerialDebug("TCP port: signal close: " + had_error);
handleCallback(had_error);

modbus.emit("close");
modbus.removeAllListeners();
self.emit("close");
self.removeAllListeners();
});

this._client.on("error", function(had_error) {
modbus.openFlag = false;
self.openFlag = false;
modbusSerialDebug("TCP port: signal error: " + had_error);
handleCallback(had_error);
});
Expand All @@ -142,7 +171,7 @@ class TcpPort extends EventEmitter {
/**
* Simulate successful port open.
*
* @param callback
* @param {(err?: Error) => void} callback
*/
open(callback) {
if(this._externalSocket === null) {
Expand All @@ -159,7 +188,7 @@ class TcpPort extends EventEmitter {
/**
* Simulate successful close port.
*
* @param callback
* @param {(err?: Error) => void} callback
*/
close(callback) {
this.callback = callback;
Expand All @@ -170,7 +199,7 @@ class TcpPort extends EventEmitter {
/**
* Simulate successful destroy port.
*
* @param callback
* @param {(err?: Error) => void} callback
*/
destroy(callback) {
this.callback = callback;
Expand All @@ -182,7 +211,7 @@ class TcpPort extends EventEmitter {
/**
* Send data to a modbus-tcp slave.
*
* @param data
* @param {Buffer} data
*/
write(data) {
if(data.length < MIN_DATA_LENGTH) {
Expand Down
Loading

0 comments on commit 9a74d61

Please sign in to comment.