modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,131 lines (1,130 loc) • 48.7 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var import_async_mutex = require("async-mutex");
var import_read_holding_registers = require("./function-codes/read-holding-registers.js");
var import_read_input_registers = require("./function-codes/read-input-registers.js");
var import_write_single_register = require("./function-codes/write-single-register.js");
var import_write_multiple_registers = require("./function-codes/write-multiple-registers.js");
var import_read_coils = require("./function-codes/read-coils.js");
var import_read_discrete_inputs = require("./function-codes/read-discrete-inputs.js");
var import_write_single_coil = require("./function-codes/write-single-coil.js");
var import_write_multiple_coils = require("./function-codes/write-multiple-coils.js");
var import_report_slave_id = require("./function-codes/report-slave-id.js");
var import_read_device_identification = require("./function-codes/read-device-identification.js");
var import_errors = require("./errors.js");
var import_constants = require("./constants/constants.js");
var import_packet_builder = require("./packet-builder.js");
var import_logger = __toESM(require("./logger.js"));
var import_crc = require("./utils/crc.js");
var import_modbus_types = require("./types/modbus-types.js");
const crcAlgorithmMap = {
crc16Modbus: import_crc.crc16Modbus,
crc16CcittFalse: import_crc.crc16CcittFalse,
crc32: import_crc.crc32,
crc8: import_crc.crc8,
crc1: import_crc.crc1,
crc8_1wire: import_crc.crc8_1wire,
crc8_dvbs2: import_crc.crc8_dvbs2,
crc16_kermit: import_crc.crc16_kermit,
crc16_xmodem: import_crc.crc16_xmodem,
crc24: import_crc.crc24,
crc32mpeg: import_crc.crc32mpeg,
crcjam: import_crc.crcjam
};
const logger = new import_logger.default();
logger.setLevel("error");
class ModbusClient {
transportController;
slaveId;
options;
rsMode;
defaultTimeout;
retryCount;
retryDelay;
echoEnabled;
crcFunc;
_mutex;
_plugins = [];
_customFunctions = /* @__PURE__ */ new Map();
_customRegisterTypes = /* @__PURE__ */ new Map();
_customCrcAlgorithms = /* @__PURE__ */ new Map();
static FUNCTION_CODE_MAP = /* @__PURE__ */ new Map([
[1, import_constants.ModbusFunctionCode.READ_COILS],
[2, import_constants.ModbusFunctionCode.READ_DISCRETE_INPUTS],
[3, import_constants.ModbusFunctionCode.READ_HOLDING_REGISTERS],
[4, import_constants.ModbusFunctionCode.READ_INPUT_REGISTERS],
[5, import_constants.ModbusFunctionCode.WRITE_SINGLE_COIL],
[6, import_constants.ModbusFunctionCode.WRITE_SINGLE_REGISTER],
[15, import_constants.ModbusFunctionCode.WRITE_MULTIPLE_COILS],
[16, import_constants.ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS],
[17, import_constants.ModbusFunctionCode.REPORT_SLAVE_ID],
[20, import_constants.ModbusFunctionCode.READ_DEVICE_COMMENT],
[21, import_constants.ModbusFunctionCode.WRITE_DEVICE_COMMENT],
[43, import_constants.ModbusFunctionCode.READ_DEVICE_IDENTIFICATION],
[82, import_constants.ModbusFunctionCode.READ_FILE_LENGTH],
[90, import_constants.ModbusFunctionCode.READ_FILE_CHUNK],
[85, import_constants.ModbusFunctionCode.OPEN_FILE],
[87, import_constants.ModbusFunctionCode.CLOSE_FILE],
[92, import_constants.ModbusFunctionCode.RESTART_CONTROLLER],
[110, import_constants.ModbusFunctionCode.GET_CONTROLLER_TIME],
[111, import_constants.ModbusFunctionCode.SET_CONTROLLER_TIME]
]);
static EXCEPTION_CODE_MAP = /* @__PURE__ */ new Map([
[1, import_constants.ModbusExceptionCode.ILLEGAL_FUNCTION],
[2, import_constants.ModbusExceptionCode.ILLEGAL_DATA_ADDRESS],
[3, import_constants.ModbusExceptionCode.ILLEGAL_DATA_VALUE],
[4, import_constants.ModbusExceptionCode.SLAVE_DEVICE_FAILURE],
[5, import_constants.ModbusExceptionCode.ACKNOWLEDGE],
[6, import_constants.ModbusExceptionCode.SLAVE_DEVICE_BUSY],
[8, import_constants.ModbusExceptionCode.MEMORY_PARITY_ERROR],
[10, import_constants.ModbusExceptionCode.GATEWAY_PATH_UNAVAILABLE],
[11, import_constants.ModbusExceptionCode.GATEWAY_TARGET_DEVICE_FAILED]
]);
constructor(transportController, slaveId = 1, options = {}) {
if (!Number.isInteger(slaveId) || slaveId < 1 || slaveId > 255) {
throw new import_errors.ModbusInvalidAddressError(slaveId);
}
this.transportController = transportController;
this.slaveId = slaveId;
this.options = options;
this.rsMode = options.RSMode ?? "RS485";
this.defaultTimeout = options.timeout ?? 2e3;
this.retryCount = options.retryCount ?? 0;
this.retryDelay = options.retryDelay ?? 100;
this.echoEnabled = options.echoEnabled ?? false;
const algorithm = options.crcAlgorithm ?? "crc16Modbus";
const crcFunc = crcAlgorithmMap[algorithm];
if (!crcFunc) {
throw new import_errors.ModbusConfigError(`Unknown CRC algorithm: ${algorithm}`);
}
this.crcFunc = crcFunc;
this._mutex = new import_async_mutex.Mutex();
this._setAutoLoggerContext();
if (options.plugins && Array.isArray(options.plugins)) {
for (const PluginClass of options.plugins) {
this.use(new PluginClass());
}
}
this._resolveCrcFunction(options.crcAlgorithm ?? "crc16Modbus");
}
/**
* Returns the effective transport for the client
*/
get _effectiveTransport() {
return this.transportController.getTransportForSlave(this.slaveId, this.rsMode);
}
/**
* Enables the ModbusClient logger
* @param level - Logging level
*/
enableLogger(level = "info") {
logger.setLevel(level);
}
/**
* Disables the ModbusClient logger (sets the highest level - error)
*/
disableLogger() {
logger.setLevel("error");
}
/**
* Sets the context for the logger (slaveId, functionCode, etc.)
* @param context - Context for the logger
*/
setLoggerContext(context) {
logger.addGlobalContext(context);
}
/**
* Sets the logger context automatically based on the current settings.
*/
_setAutoLoggerContext(funcCode) {
const transport = this._effectiveTransport;
const transportName = transport ? transport.constructor.name : "Unknown";
const context = {
slaveId: this.slaveId,
transport: transportName
};
if (funcCode !== void 0) {
context.funcCode = funcCode;
}
logger.addGlobalContext(context);
}
/**
* Registers a plugin with the ModbusClient.
* @param plugin - The plugin to register.
*/
use(plugin) {
if (!plugin || typeof plugin.name !== "string") {
throw new Error(
'Invalid plugin provided. A plugin must be an object with a "name" property.'
);
}
if (this._plugins.some((p) => p.name === plugin.name)) {
logger.warn(`Plugin with name "${plugin.name}" is already registered. Skipping.`);
return;
}
this._plugins.push(plugin);
if (plugin.customFunctionCodes) {
for (const funcName in plugin.customFunctionCodes) {
if (this._customFunctions.has(funcName)) {
logger.warn(
`Custom function "${funcName}" from plugin "${plugin.name}" overrides an existing function.`
);
}
const handler = plugin.customFunctionCodes[funcName];
if (handler) {
this._customFunctions.set(funcName, handler);
}
}
}
if (plugin.customRegisterTypes) {
for (const typeName in plugin.customRegisterTypes) {
const isBuiltIn = Object.values(import_constants.RegisterType).includes(typeName);
if (this._customRegisterTypes.has(typeName) || isBuiltIn) {
logger.warn(
`Custom register type "${typeName}" from plugin "${plugin.name}" overrides an existing type.`
);
}
const handler = plugin.customRegisterTypes[typeName];
if (handler) {
this._customRegisterTypes.set(typeName, handler);
}
}
}
if (plugin.customCrcAlgorithms) {
for (const algoName in plugin.customCrcAlgorithms) {
if (this._customCrcAlgorithms.has(algoName) || crcAlgorithmMap[algoName]) {
logger.warn(
`Custom CRC algorithm "${algoName}" from plugin "${plugin.name}" overrides an existing algorithm.`
);
}
const handler = plugin.customCrcAlgorithms[algoName];
if (handler) {
this._customCrcAlgorithms.set(algoName, handler);
}
}
const currentCrcName = this.options.crcAlgorithm ?? "crc16Modbus";
if (plugin.customCrcAlgorithms[currentCrcName]) {
this._resolveCrcFunction(currentCrcName);
}
}
logger.info(`Plugin "${plugin.name}" registered successfully.`);
}
/**
* Executes a custom function registered by a plugin.
* @param functionName - The name of the custom function to execute.
* @param args - Arguments to pass to the custom function.
* @returns The result of the custom function.
*/
async executeCustomFunction(functionName, ...args) {
const handler = this._customFunctions.get(functionName);
if (!handler) {
throw new Error(
`Custom function "${functionName}" is not registered. Have you registered the plugin using client.use()?`
);
}
const requestPdu = handler.buildRequest(...args);
const responsePdu = await this._sendRequest(requestPdu);
if (!responsePdu) {
return handler.parseResponse(new Uint8Array(0));
}
return handler.parseResponse(responsePdu);
}
/**
* Resolves the CRC function based on the provided algorithm.
* @param algorithm - The CRC algorithm to resolve.
*/
_resolveCrcFunction(algorithm) {
const customFunc = this._customCrcAlgorithms.get(algorithm);
const builtInFunc = crcAlgorithmMap[algorithm];
if (customFunc) {
this.crcFunc = customFunc;
logger.debug(`Using custom CRC algorithm "${algorithm}" from a plugin.`);
} else if (builtInFunc) {
this.crcFunc = builtInFunc;
} else {
throw new import_errors.ModbusConfigError(`Unknown CRC algorithm: ${algorithm}`);
}
}
/**
* Performs a logical connection check to ensure the client is ready for communication.
* This method verifies that a transport is available and has been connected by the TransportController.
* It does NOT initiate the physical connection itself.
* Throws ModbusNotConnectedError if the transport is not ready.
*/
async connect() {
const release = await this._mutex.acquire();
try {
const transport = this._effectiveTransport;
if (!transport) {
throw new import_errors.ModbusNotConnectedError();
}
if (!transport.isOpen) {
throw new import_errors.ModbusNotConnectedError();
}
this._setAutoLoggerContext();
logger.info("Client is ready. Transport is connected and available.", {
slaveId: this.slaveId,
transport: transport.constructor.name
});
} finally {
release();
}
}
/**
* Performs a logical disconnection for the client.
* This method is a no-op regarding the physical transport layer, which should be managed
* exclusively by the TransportController. It simply logs the client's logical disconnection.
*/
async disconnect() {
const release = await this._mutex.acquire();
try {
const transport = this._effectiveTransport;
this._setAutoLoggerContext();
logger.info(
"Client logically disconnected. The physical transport connection is not affected.",
{
slaveId: this.slaveId,
transport: transport ? transport.constructor.name : "N/A"
}
);
} finally {
release();
}
}
/**
* Converts a buffer to a hex string.
* @param buffer - The buffer to convert.
* @returns The hex string representation of the buffer.
*/
_toHex(buffer) {
return Array.from(buffer).map((b) => b.toString(16).padStart(2, "0")).join(" ");
}
/**
* Calculates the expected response length based on the PDU.
* @param pdu - The PDU to calculate the expected response length for.
* @returns The expected response length, or null if the PDU is invalid.
*/
_getExpectedResponseLength(pdu) {
if (!pdu || pdu.length === 0) return null;
const funcCode = pdu[0];
const modbusFuncCode = ModbusClient.FUNCTION_CODE_MAP.get(funcCode);
if (!modbusFuncCode) {
if (funcCode & 128) {
return 5;
}
return null;
}
switch (modbusFuncCode) {
case import_constants.ModbusFunctionCode.READ_COILS:
case import_constants.ModbusFunctionCode.READ_DISCRETE_INPUTS: {
if (pdu.length < 5) return null;
const bitCount = pdu[3] << 8 | pdu[4];
if (bitCount < 1 || bitCount > 2e3) {
throw new import_errors.ModbusInvalidQuantityError(bitCount, 1, 2e3);
}
return 5 + Math.ceil(bitCount / 8);
}
case import_constants.ModbusFunctionCode.READ_HOLDING_REGISTERS:
case import_constants.ModbusFunctionCode.READ_INPUT_REGISTERS: {
if (pdu.length < 5) return null;
const regCount = pdu[3] << 8 | pdu[4];
if (regCount < 1 || regCount > 125) {
throw new import_errors.ModbusInvalidQuantityError(regCount, 1, 125);
}
return 5 + regCount * 2;
}
case import_constants.ModbusFunctionCode.WRITE_SINGLE_COIL:
case import_constants.ModbusFunctionCode.WRITE_SINGLE_REGISTER:
return 8;
case import_constants.ModbusFunctionCode.WRITE_MULTIPLE_COILS:
case import_constants.ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS:
return 8;
case import_constants.ModbusFunctionCode.READ_DEVICE_COMMENT:
return null;
case import_constants.ModbusFunctionCode.WRITE_DEVICE_COMMENT:
return 5;
case import_constants.ModbusFunctionCode.READ_DEVICE_IDENTIFICATION: {
if (pdu.length < 4) return null;
if (pdu[2] === 0) return 6;
if (pdu[2] === 4) return null;
return null;
}
case import_constants.ModbusFunctionCode.READ_FILE_LENGTH:
return 8;
case import_constants.ModbusFunctionCode.OPEN_FILE:
return 8;
case import_constants.ModbusFunctionCode.CLOSE_FILE:
return 5;
case import_constants.ModbusFunctionCode.RESTART_CONTROLLER:
return 0;
case import_constants.ModbusFunctionCode.GET_CONTROLLER_TIME:
return 10;
case import_constants.ModbusFunctionCode.SET_CONTROLLER_TIME:
return 8;
default:
return null;
}
}
/**
* Reads a packet from the Modbus transport.
* @param timeout - The timeout in milliseconds.
* @param requestPdu - The PDU of the request packet.
* @returns The received packet.
* @throws ModbusTimeoutError If the read operation times out.
*/
async _readPacket(timeout, requestPdu = null) {
const start = Date.now();
let buffer = new Uint8Array(0);
const expectedLength = requestPdu ? this._getExpectedResponseLength(requestPdu) : null;
while (true) {
const timeLeft = timeout - (Date.now() - start);
if (timeLeft <= 0) throw new import_errors.ModbusTimeoutError("Read timeout");
const minPacketLength = 5;
const bytesToRead = expectedLength ? Math.max(1, expectedLength - buffer.length) : Math.max(1, minPacketLength - buffer.length);
const transport = this._effectiveTransport;
if (!transport) {
throw new Error(`No transport available for slaveId ${this.slaveId}`);
}
const chunk = await transport.read(bytesToRead, timeLeft);
if (!chunk || chunk.length === 0) continue;
const newBuffer = new Uint8Array(buffer.length + chunk.length);
newBuffer.set(buffer, 0);
newBuffer.set(chunk, buffer.length);
buffer = newBuffer;
const funcCode = requestPdu ? requestPdu[0] : void 0;
this._setAutoLoggerContext(funcCode);
logger.debug("Received chunk:", { bytes: chunk.length, total: buffer.length });
if (buffer.length >= minPacketLength) {
try {
(0, import_packet_builder.parsePacket)(buffer, this.crcFunc);
return buffer;
} catch (err) {
if (err instanceof import_errors.ModbusCRCError) {
logger.error("CRC mismatch detected");
continue;
} else if (err instanceof import_errors.ModbusFramingError) {
logger.error("Framing error detected");
continue;
} else if (err instanceof import_errors.ModbusParityError) {
logger.error("Parity error detected");
continue;
} else if (err instanceof import_errors.ModbusNoiseError) {
logger.error("Noise error detected");
continue;
} else if (err instanceof import_errors.ModbusOverrunError) {
logger.error("Overrun error detected");
continue;
} else if (err instanceof import_errors.ModbusCollisionError) {
logger.error("Collision error detected");
continue;
} else if (err instanceof import_errors.ModbusSyncError) {
logger.error("Sync error detected");
continue;
} else if (err instanceof import_errors.ModbusFrameBoundaryError) {
logger.error("Frame boundary error detected");
continue;
} else if (err instanceof import_errors.ModbusLRCError) {
logger.error("LRC error detected");
continue;
} else if (err instanceof import_errors.ModbusChecksumError) {
logger.error("Checksum error detected");
continue;
} else if (err instanceof import_errors.ModbusMalformedFrameError) {
logger.error("Malformed frame error detected");
continue;
} else if (err instanceof import_errors.ModbusInvalidFrameLengthError) {
logger.error("Invalid frame length error detected");
continue;
} else if (err instanceof import_errors.ModbusInvalidTransactionIdError) {
logger.error("Invalid transaction ID error detected");
continue;
} else if (err instanceof import_errors.ModbusUnexpectedFunctionCodeError) {
logger.error("Unexpected function code error detected");
continue;
} else if (err instanceof import_errors.ModbusTooManyEmptyReadsError) {
logger.error("Too many empty reads error detected");
continue;
} else if (err instanceof import_errors.ModbusInterFrameTimeoutError) {
logger.error("Inter-frame timeout error detected");
continue;
} else if (err instanceof import_errors.ModbusSilentIntervalError) {
logger.error("Silent interval error detected");
continue;
} else if (err instanceof import_errors.ModbusResponseError) {
logger.error("Response error detected");
continue;
} else if (err instanceof import_errors.ModbusBufferOverflowError) {
logger.error("Buffer overflow error detected");
continue;
} else if (err instanceof import_errors.ModbusBufferUnderrunError) {
logger.error("Buffer underrun error detected");
continue;
} else if (err instanceof import_errors.ModbusMemoryError) {
logger.error("Memory error detected");
continue;
} else if (err instanceof import_errors.ModbusStackOverflowError) {
logger.error("Stack overflow error detected");
continue;
} else if (err instanceof import_errors.ModbusInsufficientDataError) {
logger.error("Insufficient data error detected");
continue;
} else if (err instanceof Error && err.message.startsWith("Invalid packet: too short")) {
continue;
} else if (err instanceof Error && err.message.startsWith("CRC mismatch")) {
continue;
} else {
throw err;
}
}
}
}
}
/**
* Sends a request to the Modbus transport.
* @param pdu - The PDU of the request packet.
* @param timeout - The timeout in milliseconds.
* @param ignoreNoResponse - Whether to ignore no response.
* @returns The received packet or undefined if no response is expected.
* @throws ModbusTimeoutError If the send operation times out.
*/
async _sendRequest(pdu, timeout = this.defaultTimeout, ignoreNoResponse = false) {
const release = await this._mutex.acquire();
try {
const funcCode = pdu[0];
const funcCodeEnum = ModbusClient.FUNCTION_CODE_MAP.get(funcCode) ?? funcCode;
const slaveId = this.slaveId;
let lastError;
const startTime = Date.now();
for (let attempt = 0; attempt <= this.retryCount; attempt++) {
const transport = this._effectiveTransport;
if (!transport) {
throw new Error(`No transport available for slaveId ${this.slaveId}`);
}
const allTransports = this.transportController.listTransports();
const transportInfo = allTransports.find((t) => t.transport === transport);
const transportId = transportInfo?.id;
try {
const attemptStart = Date.now();
const timeLeft = timeout - (attemptStart - startTime);
if (timeLeft <= 0) throw new import_errors.ModbusTimeoutError("Timeout before request");
this._setAutoLoggerContext(funcCodeEnum);
logger.debug(`Attempt #${attempt + 1} \u2014 sending request`, {
slaveId,
funcCode
});
const packet = (0, import_packet_builder.buildPacket)(slaveId, pdu, this.crcFunc);
const attemptLogic = async () => {
if (transport.flush) {
try {
await transport.flush();
} finally {
}
}
await transport.write(packet);
logger.debug("Packet written to transport", {
bytes: packet.length,
slaveId,
funcCode
});
if (this.echoEnabled) {
logger.debug("Echo enabled, reading echo back...", { slaveId, funcCode });
const echoResponse = await transport.read(packet.length, timeLeft);
if (!echoResponse || echoResponse.length !== packet.length) {
throw new import_errors.ModbusInsufficientDataError(
echoResponse ? echoResponse.length : 0,
packet.length
);
}
for (let i = 0; i < packet.length; i++) {
if (packet[i] !== echoResponse[i]) {
throw new Error("Echo mismatch detected");
}
}
logger.debug("Echo verified successfully", { slaveId, funcCode });
}
if (ignoreNoResponse) {
return void 0;
}
return await this._readPacket(timeLeft, pdu);
};
let response;
if (transportId && this.transportController.executeImmediate) {
response = await this.transportController.executeImmediate(transportId, attemptLogic);
} else {
response = await attemptLogic();
}
if (ignoreNoResponse) {
const elapsed2 = Date.now() - startTime;
logger.info("Request sent, no response expected", {
slaveId,
funcCode,
responseTime: elapsed2
});
return void 0;
}
if (!response) {
throw new Error("No response received");
}
const elapsed = Date.now() - startTime;
const { slaveAddress, pdu: responsePdu } = (0, import_packet_builder.parsePacket)(response, this.crcFunc);
if (slaveAddress !== slaveId) {
throw new Error(`Slave address mismatch (expected ${slaveId}, got ${slaveAddress})`);
}
if (transport.notifyDeviceConnected) {
transport.notifyDeviceConnected(slaveId);
}
const responseFuncCode = responsePdu[0];
if ((responseFuncCode & 128) !== 0) {
const exceptionCode = responsePdu[1];
const modbusExceptionCode = ModbusClient.EXCEPTION_CODE_MAP.get(exceptionCode) ?? exceptionCode;
const exceptionMessage = import_constants.MODBUS_EXCEPTION_MESSAGES[exceptionCode] ?? `Unknown exception code: ${exceptionCode}`;
logger.warn("Modbus exception received", {
slaveId,
funcCode,
exceptionCode,
exceptionMessage,
responseTime: elapsed
});
throw new import_errors.ModbusExceptionError(responseFuncCode & 127, modbusExceptionCode);
}
logger.info("Response received", {
slaveId,
funcCode,
responseTime: elapsed
});
return responsePdu;
} catch (err) {
const elapsed = Date.now() - startTime;
if (!(err instanceof import_errors.ModbusExceptionError)) {
if (transport.notifyDeviceDisconnected) {
let errorType = import_modbus_types.ConnectionErrorType.UnknownError;
if (err instanceof import_errors.ModbusTimeoutError) {
errorType = import_modbus_types.ConnectionErrorType.Timeout;
} else if (err instanceof import_errors.ModbusCRCError) {
errorType = import_modbus_types.ConnectionErrorType.CRCError;
}
const errorMessage = err instanceof Error ? err.message : String(err);
transport.notifyDeviceDisconnected(slaveId, errorType, errorMessage);
}
}
const isFlushedError = err instanceof import_errors.ModbusFlushError;
const errorCode = err instanceof Error && err.message.toLowerCase().includes("timeout") ? "timeout" : err instanceof Error && err.message.toLowerCase().includes("crc") ? "crc" : err instanceof import_errors.ModbusExceptionError ? "modbus-exception" : null;
if (transport.flush) {
try {
await transport.flush();
logger.debug("Transport flushed after error", { slaveId });
} catch (flushErr) {
logger.warn("Failed to flush transport after error", {
slaveId,
flushError: flushErr
});
}
}
this._setAutoLoggerContext(funcCodeEnum);
logger.warn(
`Attempt #${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`,
{
responseTime: elapsed,
error: err,
requestHex: this._toHex(pdu),
slaveId,
funcCode,
errorCode,
exceptionCode: err instanceof import_errors.ModbusExceptionError ? err.exceptionCode : null
}
);
lastError = err;
if (isFlushedError) {
logger.info(`Attempt #${attempt + 1} failed due to flush, will retry`, {
slaveId,
funcCode
});
}
if (ignoreNoResponse && err instanceof Error && err.message.toLowerCase().includes("timeout") || isFlushedError) {
logger.info("Operation ignored due to ignoreNoResponse=true and timeout/flush", {
slaveId,
funcCode,
responseTime: elapsed
});
return void 0;
}
if (attempt < this.retryCount) {
let delay = this.retryDelay;
if (isFlushedError) {
delay = Math.min(50, delay);
logger.debug(`Retrying after short delay ${delay}ms due to flush`, {
slaveId,
funcCode
});
} else {
logger.debug(`Retrying after delay ${delay}ms`, { slaveId, funcCode });
}
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
if (transport.flush) {
try {
await transport.flush();
logger.debug("Final transport flush after all retries failed", { slaveId });
} catch (flushErr) {
logger.warn("Failed to final flush transport", { slaveId, flushError: flushErr });
}
}
logger.error(`All ${this.retryCount + 1} attempts exhausted`, {
error: lastError,
slaveId,
funcCode,
responseTime: elapsed
});
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
}
}
throw new Error("Unexpected end of _sendRequest function");
} finally {
release();
}
}
/**
* Converts Modbus registers to the specified type.
* @param registers - The registers to convert.
* @param type - The type of the registers.
* @returns The converted registers.
*/
_convertRegisters(registers, type = import_constants.RegisterType.UINT16) {
if (!registers || !Array.isArray(registers)) {
throw new import_errors.ModbusDataConversionError(registers, "non-empty array");
}
const customTypeHandler = this._customRegisterTypes.get(type);
if (customTypeHandler) {
return customTypeHandler(registers);
}
const buffer = new ArrayBuffer(registers.length * 2);
const view = new DataView(buffer);
registers.forEach((reg, i) => {
view.setUint16(i * 2, reg, false);
});
const read32 = (method, littleEndian = false) => {
const result = [];
for (let i = 0; i < registers.length - 1; i += 2) {
result.push(view[method](i * 2, littleEndian));
}
return result;
};
const read64 = (method, littleEndian = false) => {
if (method === "getFloat64") {
const result2 = [];
for (let i = 0; i < registers.length - 3; i += 4) {
const tempBuf = new ArrayBuffer(8);
const tempView = new DataView(tempBuf);
for (let j = 0; j < 8; j++) {
tempView.setUint8(j, view.getUint8(i * 2 + j));
}
result2.push(tempView.getFloat64(0, littleEndian));
}
return result2;
}
const result = [];
for (let i = 0; i < registers.length - 3; i += 4) {
const tempBuf = new ArrayBuffer(8);
const tempView = new DataView(tempBuf);
for (let j = 0; j < 8; j++) {
tempView.setUint8(j, view.getUint8(i * 2 + j));
}
const high = BigInt(tempView.getUint32(0, littleEndian));
const low = BigInt(tempView.getUint32(4, littleEndian));
let value = high << 32n | low;
if (method === "getInt64" && value & 1n << 63n) {
value -= 1n << 64n;
}
result.push(value);
}
return result;
};
const getSwapped32 = (i, mode) => {
const a = view.getUint8(i * 2);
const b = view.getUint8(i * 2 + 1);
const c = view.getUint8(i * 2 + 2);
const d = view.getUint8(i * 2 + 3);
let bytes;
switch (mode) {
case "sw":
bytes = [c, d, a, b];
break;
case "sb":
bytes = [b, a, d, c];
break;
case "sbw":
bytes = [d, c, b, a];
break;
case "le":
bytes = [d, c, b, a];
break;
case "le_sw":
bytes = [b, a, d, c];
break;
case "le_sb":
bytes = [a, b, c, d];
break;
case "le_sbw":
bytes = [c, d, a, b];
break;
default:
bytes = [a, b, c, d];
break;
}
const tempBuf = new ArrayBuffer(4);
const tempView = new DataView(tempBuf);
bytes.forEach((byte, idx) => tempView.setUint8(idx, byte));
return tempView;
};
const read32Swapped = (method, mode) => {
const result = [];
for (let i = 0; i < registers.length - 1; i += 2) {
const tempView = getSwapped32(i, mode);
result.push(tempView[method](0, false));
}
return result;
};
switch (type) {
case import_constants.RegisterType.UINT16:
return registers;
case import_constants.RegisterType.INT16:
return registers.map((_, i) => view.getInt16(i * 2, false));
case import_constants.RegisterType.UINT32:
return read32("getUint32");
case import_constants.RegisterType.INT32:
return read32("getInt32");
case import_constants.RegisterType.FLOAT:
return read32("getFloat32");
case import_constants.RegisterType.UINT32_LE:
return read32("getUint32", true);
case import_constants.RegisterType.INT32_LE:
return read32("getInt32", true);
case import_constants.RegisterType.FLOAT_LE:
return read32("getFloat32", true);
case import_constants.RegisterType.UINT32_SW:
return read32Swapped("getUint32", "sw");
case import_constants.RegisterType.INT32_SW:
return read32Swapped("getInt32", "sw");
case import_constants.RegisterType.FLOAT_SW:
return read32Swapped("getFloat32", "sw");
case import_constants.RegisterType.UINT32_SB:
return read32Swapped("getUint32", "sb");
case import_constants.RegisterType.INT32_SB:
return read32Swapped("getInt32", "sb");
case import_constants.RegisterType.FLOAT_SB:
return read32Swapped("getFloat32", "sb");
case import_constants.RegisterType.UINT32_SBW:
return read32Swapped("getUint32", "sbw");
case import_constants.RegisterType.INT32_SBW:
return read32Swapped("getInt32", "sbw");
case import_constants.RegisterType.FLOAT_SBW:
return read32Swapped("getFloat32", "sbw");
case import_constants.RegisterType.UINT32_LE_SW:
return read32Swapped("getUint32", "le_sw");
case import_constants.RegisterType.INT32_LE_SW:
return read32Swapped("getInt32", "le_sw");
case import_constants.RegisterType.FLOAT_LE_SW:
return read32Swapped("getFloat32", "le_sw");
case import_constants.RegisterType.UINT32_LE_SB:
return read32Swapped("getUint32", "le_sb");
case import_constants.RegisterType.INT32_LE_SB:
return read32Swapped("getInt32", "le_sb");
case import_constants.RegisterType.FLOAT_LE_SB:
return read32Swapped("getFloat32", "le_sb");
case import_constants.RegisterType.UINT32_LE_SBW:
return read32Swapped("getUint32", "le_sbw");
case import_constants.RegisterType.INT32_LE_SBW:
return read32Swapped("getInt32", "le_sbw");
case import_constants.RegisterType.FLOAT_LE_SBW:
return read32Swapped("getFloat32", "le_sbw");
case import_constants.RegisterType.UINT64:
return read64("getUint64");
case import_constants.RegisterType.INT64:
return read64("getInt64");
case import_constants.RegisterType.DOUBLE:
return read64("getFloat64");
case import_constants.RegisterType.UINT64_LE:
return read64("getUint64", true);
case import_constants.RegisterType.INT64_LE:
return read64("getInt64", true);
case import_constants.RegisterType.DOUBLE_LE:
return read64("getFloat64", true);
case import_constants.RegisterType.HEX:
return registers.map(
(r) => r.toString(16).toUpperCase().padStart(4, "0")
);
case import_constants.RegisterType.STRING: {
let str = "";
for (let i = 0; i < registers.length; i++) {
const high = registers[i] >> 8 & 255;
const low = registers[i] & 255;
if (high !== 0) str += String.fromCharCode(high);
if (low !== 0) str += String.fromCharCode(low);
}
return [str];
}
case import_constants.RegisterType.BOOL:
return registers.map((r) => r !== 0);
case import_constants.RegisterType.BINARY:
return registers.map(
(r) => r.toString(2).padStart(16, "0").split("").map((b) => b === "1")
);
case import_constants.RegisterType.BCD:
return registers.map((r) => {
const high = r >> 8 & 255;
const low = r & 255;
return ((high >> 4) * 10 + (high & 15)) * 100 + (low >> 4) * 10 + (low & 15);
});
default:
throw new import_errors.ModbusDataConversionError(type, "a supported built-in or custom register type");
}
}
/**
* Reads holding registers from the Modbus device.
* @param startAddress - The starting address of the registers to read.
* @param quantity - The number of registers to read.
* @param options - Optional options for the conversion.
* @returns The converted registers.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
*/
async readHoldingRegisters(startAddress, quantity, options = {}) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 125) {
throw new import_errors.ModbusInvalidQuantityError(quantity, 1, 125);
}
const pdu = (0, import_read_holding_registers.buildReadHoldingRegistersRequest)(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu);
if (!responsePdu) {
throw new Error("No response received");
}
const registers = (0, import_read_holding_registers.parseReadHoldingRegistersResponse)(responsePdu);
const type = options.type ?? import_constants.RegisterType.UINT16;
return this._convertRegisters(registers, type);
}
/**
* Reads input registers from the Modbus device.
* @param startAddress - The starting address of the registers to read.
* @param quantity - The number of registers to read.
* @param options - Optional options for the conversion.
* @returns The converted registers.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
*/
async readInputRegisters(startAddress, quantity, options = {}) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 125) {
throw new import_errors.ModbusInvalidQuantityError(quantity, 1, 125);
}
const pdu = (0, import_read_input_registers.buildReadInputRegistersRequest)(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu);
if (!responsePdu) {
throw new Error("No response received");
}
const registers = (0, import_read_input_registers.parseReadInputRegistersResponse)(responsePdu);
const type = options.type ?? import_constants.RegisterType.UINT16;
return this._convertRegisters(registers, type);
}
/**
* Writes a single register to the Modbus device.
* @param address - The address of the register to write.
* @param value - The value to write to the register.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the address is invalid.
* @throws ModbusIllegalDataValueError If the value is invalid.
*/
async writeSingleRegister(address, value, timeout) {
if (!Number.isInteger(address) || address < 0 || address > 65535) {
throw new import_errors.ModbusInvalidAddressError(address);
}
if (!Number.isInteger(value) || value < 0 || value > 65535) {
throw new import_errors.ModbusIllegalDataValueError(value, "integer between 0 and 65535");
}
const pdu = (0, import_write_single_register.buildWriteSingleRegisterRequest)(address, value);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_write_single_register.parseWriteSingleRegisterResponse)(responsePdu);
}
/**
* Writes multiple registers to the Modbus device.
* @param startAddress - The starting address of the registers to write.
* @param values - The values to write to the registers.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
* @throws ModbusIllegalDataValueError If any of the values are invalid.
*/
async writeMultipleRegisters(startAddress, values, timeout) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Array.isArray(values) || values.length < 1 || values.length > 123) {
throw new import_errors.ModbusInvalidQuantityError(values.length, 1, 123);
}
if (values.some((v) => !Number.isInteger(v) || v < 0 || v > 65535)) {
const invalidValue = values.find((v) => !Number.isInteger(v) || v < 0 || v > 65535);
throw new import_errors.ModbusIllegalDataValueError(invalidValue, "integer between 0 and 65535");
}
const pdu = (0, import_write_multiple_registers.buildWriteMultipleRegistersRequest)(startAddress, values);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_write_multiple_registers.parseWriteMultipleRegistersResponse)(responsePdu);
}
/**
* Reads coils from the Modbus device.
* @param startAddress - The starting address of the coils to read.
* @param quantity - The number of coils to read.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
*/
async readCoils(startAddress, quantity, timeout) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 2e3) {
throw new import_errors.ModbusInvalidQuantityError(quantity, 1, 2e3);
}
const pdu = (0, import_read_coils.buildReadCoilsRequest)(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_read_coils.parseReadCoilsResponse)(responsePdu);
}
/**
* Reads discrete inputs from the Modbus device.
* @param startAddress - The starting address of the discrete inputs to read.
* @param quantity - The number of discrete inputs to read.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
*/
async readDiscreteInputs(startAddress, quantity, timeout) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 2e3) {
throw new import_errors.ModbusInvalidQuantityError(quantity, 1, 2e3);
}
const pdu = (0, import_read_discrete_inputs.buildReadDiscreteInputsRequest)(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_read_discrete_inputs.parseReadDiscreteInputsResponse)(responsePdu);
}
/**
* Writes a single coil to the Modbus device.
* @param address - The address of the coil to write.
* @param value - The value to write to the coil (boolean or 0/1).
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the address is invalid.
* @throws ModbusIllegalDataValueError If the value is invalid.
*/
async writeSingleCoil(address, value, timeout) {
if (!Number.isInteger(address) || address < 0 || address > 65535) {
throw new import_errors.ModbusInvalidAddressError(address);
}
if (typeof value === "number" && value !== 0 && value !== 1) {
throw new import_errors.ModbusIllegalDataValueError(value, "boolean or 0/1");
}
const pdu = (0, import_write_single_coil.buildWriteSingleCoilRequest)(address, value);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_write_single_coil.parseWriteSingleCoilResponse)(responsePdu);
}
/**
* Writes multiple coils to the Modbus device.
* @param startAddress - The starting address of the coils to write.
* @param values - The values to write to the coils (array of booleans or 0/1).
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
* @throws ModbusInvalidAddressError If the start address is invalid.
* @throws ModbusInvalidQuantityError If the quantity is invalid.
* @throws ModbusIllegalDataValueError If any of the values are invalid.
*/
async writeMultipleCoils(startAddress, values, timeout) {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new import_errors.ModbusInvalidAddressError(startAddress);
}
if (!Array.isArray(values) || values.length < 1 || values.length > 1968) {
throw new import_errors.ModbusInvalidQuantityError(values.length, 1, 1968);
}
const pdu = (0, import_write_multiple_coils.buildWriteMultipleCoilsRequest)(startAddress, values);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_write_multiple_coils.parseWriteMultipleCoilsResponse)(responsePdu);
}
/**
* Reports the slave ID of the Modbus device.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
*/
async reportSlaveId(timeout) {
const pdu = (0, import_report_slave_id.buildReportSlaveIdRequest)();
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_report_slave_id.parseReportSlaveIdResponse)(responsePdu);
}
/**
* Reads device identification from the Modbus device.
* @param timeout - Optional timeout for the request.
* @returns The response from the device.
*/
async readDeviceIdentification(timeout) {
const originalSlaveId = this.slaveId;
try {
const pdu = (0, import_read_device_identification.buildReadDeviceIdentificationRequest)();
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error("No response received");
}
return (0, import_read_device_identification.parseReadDeviceIdentificationResponse)(responsePdu);
} finally {
this.slaveId = originalSlaveId;
}
}
}
module.exports = ModbusClient;