modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,318 lines (1,267 loc) • 55.2 kB
text/typescript
// src/client.ts
// ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
// ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
// ⣿⣿⣿⣿⢹⣿⣿⣿⣿⣿⣿⣿⣿⡧⠤⢼⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿
// ⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⡿⠿⡗⠒⢺⣿⠿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿
// ⣿⣿⣿⡇⢸⡏⢸⠙⡿⠛⠉⠄⠄⣋⣉⣉⣿⠄⣶⣍⡻⢿⠏⡇⢹⡇⢻⣿⣿⣿
// ⣿⣿⣿⡇⢸⡇⠈⡇⡇⠄⠄⠄⢠⠤⠤⠤⠿⡄⠸⣿⣿⣾⢰⠃⠘⣇⢸⣿⣿⣿
// ⣿⣿⣿⠃⢸⠁⠄⢧⢸⠄⠄⠄⢸⠤⠤⠤⢾⡇⠄⠹⣿⡏⢸⠄⠄⢻⠈⣿⣿⣿
// ⣿⣿⡿⠄⡏⠄⠄⢸⠘⡄⣀⠠⢞⠒⠒⠒⢺⣿⣄⡀⠈⡅⡏⠄⠄⢸⡄⢹⣿⣿
// ⣿⣿⡇⠄⡇⠄⢀⡠⠖⠋⠁⣰⣿⣷⣒⣒⣺⣿⣿⣿⣷⣦⣇⠄⠄⠘⡇⠸⣿⣿
// ⣿⣿⡡⢴⠃⢾⠓⠒⠢⠤⠼⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠛⠲⠄⡷⢄⣿⣿⣿
// ⣿⡏⠁⢸⠄⠈⣿⣿⠉⠓⣶⣶⡖⠒⠒⠒⠒⢒⣶⣶⡒⠛⢹⣿ ⠁⠄⡇⠄⣿
// ⣿⡇⠄⢸⠄⠄⠻⡛⢄⣠⣤⣤⣥⡔⡏⣉⡟⣦⣭⣭⣅⠄⠘⡻⠄⠄⣿⠄⠄⣿
// ⣿⡇⠄⢸⠄⠄⠄⠙⢄⣙⣿⡿⠿⠛⠉⡟⠁⠛⠿⣿⣏⣁⠞⠁⠄⠄⢻⠄⠄⣿
// ⣿⡇⠄⢸⠄⠄⠄⠄⢸⣿⡇⠄⠄⠄⠄⡇⠄⠄⠄⠄⣿⣿⠄⠄⠄⠄⢸⠄⠄⣿
// ⣿⡇⠄⣸⠄⠄⠄⠄⢸⣿⡇⠄⠄⠄⠄⡇⠄⠄⠄⠄⣿⣿⠄⠄⠄⠄⢸⠄⠄⣿
// ⣿⣿⣦⣿⠄⠄⠄⠄⢸⣿⡇⠄⠄⠄⠄⡇⠄⠄⠄⠄⣿⣿⠄⠄⠄⠄⢸⣤⣾⣿
// ⣿⣿⣿⣿⠄⠄⣠⠴⢊⣽⠃⠄⠄⠄⠄⡇⠄⠄⠄⠄⢩⡉⠲⢄⡀⠄⣸⣿⣿⣿
// ⣿⣿⣿⣿⣷⣮⣁⣠⣿⣿⣿⣶⣦⣀⡀⡇⢀⣠⣶⣿⣿⣿⣦⣀⣽⣾⣿⣿⣿⣿
// ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
// ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
import { Mutex } from 'async-mutex';
// Registers
import {
buildReadHoldingRegistersRequest,
parseReadHoldingRegistersResponse,
} from './function-codes/read-holding-registers.js';
import {
buildReadInputRegistersRequest,
parseReadInputRegistersResponse,
} from './function-codes/read-input-registers.js';
import {
buildWriteSingleRegisterRequest,
parseWriteSingleRegisterResponse,
} from './function-codes/write-single-register.js';
import {
buildWriteMultipleRegistersRequest,
parseWriteMultipleRegistersResponse,
} from './function-codes/write-multiple-registers.js';
// Bit operations
import { buildReadCoilsRequest, parseReadCoilsResponse } from './function-codes/read-coils.js';
import {
buildReadDiscreteInputsRequest,
parseReadDiscreteInputsResponse,
} from './function-codes/read-discrete-inputs.js';
import {
buildWriteSingleCoilRequest,
parseWriteSingleCoilResponse,
} from './function-codes/write-single-coil.js';
import {
buildWriteMultipleCoilsRequest,
parseWriteMultipleCoilsResponse,
} from './function-codes/write-multiple-coils.js';
// Special functions
import {
buildReportSlaveIdRequest,
parseReportSlaveIdResponse,
} from './function-codes/report-slave-id.js';
import {
buildReadDeviceIdentificationRequest,
parseReadDeviceIdentificationResponse,
} from './function-codes/read-device-identification.js';
// Special functions for SGM130
import {
buildReadFileLengthRequest,
parseReadFileLengthResponse,
} from './function-codes/SGM130/read-file-length.js';
import { buildOpenFileRequest, parseOpenFileResponse } from './function-codes/SGM130/openFile.js';
import {
buildCloseFileRequest,
parseCloseFileResponse,
} from './function-codes/SGM130/closeFile.js';
import {
buildRestartControllerRequest,
parseRestartControllerResponse,
} from './function-codes/SGM130/restart-controller.js';
import {
buildGetControllerTimeRequest,
parseGetControllerTimeResponse,
} from './function-codes/SGM130/get-controller-time.js';
import {
buildSetControllerTimeRequest,
parseSetControllerTimeResponse,
} from './function-codes/SGM130/set-controller-time.js';
import {
ModbusTimeoutError,
ModbusExceptionError,
ModbusFlushError,
ModbusCRCError,
ModbusFramingError,
ModbusOverrunError,
ModbusCollisionError,
ModbusConfigError,
ModbusSyncError,
ModbusFrameBoundaryError,
ModbusLRCError,
ModbusChecksumError,
ModbusDataConversionError,
ModbusBufferOverflowError,
ModbusBufferUnderrunError,
ModbusMemoryError,
ModbusStackOverflowError,
ModbusResponseError,
ModbusInvalidAddressError,
ModbusInvalidQuantityError,
ModbusIllegalDataValueError,
ModbusMalformedFrameError,
ModbusInvalidFrameLengthError,
ModbusInvalidTransactionIdError,
ModbusUnexpectedFunctionCodeError,
ModbusConnectionRefusedError,
ModbusConnectionTimeoutError,
ModbusNotConnectedError,
ModbusAlreadyConnectedError,
ModbusInsufficientDataError,
ModbusTooManyEmptyReadsError,
ModbusInterFrameTimeoutError,
ModbusSilentIntervalError,
ModbusParityError,
ModbusNoiseError,
} from './errors.js';
import {
ModbusFunctionCode,
ModbusExceptionCode,
MODBUS_EXCEPTION_MESSAGES,
RegisterType,
} from './constants/constants.js';
import { buildPacket, parsePacket } from './packet-builder.js';
import Logger from './logger.js';
import { Diagnostics } from './utils/diagnostics.js';
import {
crc16Modbus,
crc16CcittFalse,
crc32,
crc8,
crc1,
crc8_1wire,
crc8_dvbs2,
crc16_kermit,
crc16_xmodem,
crc24,
crc32mpeg,
crcjam,
} from './utils/crc.js';
import {
Transport,
ModbusClientOptions,
ConvertRegisterOptions,
ControllerTime,
LogContext as LoggerContext,
ConvertedRegisters,
ReadCoilsResponse,
ReadDiscreteInputsResponse,
WriteSingleCoilResponse,
WriteMultipleCoilsResponse,
WriteSingleRegisterResponse,
WriteMultipleRegistersResponse,
ReportSlaveIdResponse,
ReadDeviceIdentificationResponse,
ReadFileLengthResponse,
OpenFileResponse,
CloseFileResponse,
RestartControllerResponse,
GetControllerTimeResponse,
SetControllerTimeResponse,
} from './types/modbus-types.js';
// Import TransportController
import TransportController from './transport/transport-controller.js';
// Type for CRC function
type CrcFunction = (data: Uint8Array) => Uint8Array;
// Map of explicitly typed CRC algorithms
const crcAlgorithmMap: Record<string, CrcFunction> = {
crc16Modbus,
crc16CcittFalse,
crc32,
crc8,
crc1,
crc8_1wire,
crc8_dvbs2,
crc16_kermit,
crc16_xmodem,
crc24,
crc32mpeg,
crcjam,
};
const logger = new Logger();
logger.setLevel('error');
type DataViewMethod32 = 'getUint32' | 'getInt32' | 'getFloat32';
type DataViewMethod64 = 'getUint64' | 'getInt64' | 'getFloat64';
type SwapMode = 'sw' | 'sb' | 'sbw' | 'le' | 'le_sw' | 'le_sb' | 'le_sbw';
// Интерфейс расширенного транспорта для отслеживания состояния устройств
interface ExtendedTransport extends Transport {
notifyDeviceConnected?(slaveId: number): void;
notifyDeviceDisconnected?(slaveId: number, errorType?: string, errorMessage?: string): void;
}
class ModbusClient {
private transport: ExtendedTransport | null;
private transportController: TransportController | null;
private slaveId: number;
private defaultTimeout: number;
private retryCount: number;
private retryDelay: number;
private echoEnabled: boolean;
private diagnosticsEnabled: boolean;
private diagnostics: Diagnostics;
private crcFunc: CrcFunction;
private _mutex: Mutex;
private static readonly FUNCTION_CODE_MAP = new Map<number, ModbusFunctionCode>([
[0x01, ModbusFunctionCode.READ_COILS],
[0x02, ModbusFunctionCode.READ_DISCRETE_INPUTS],
[0x03, ModbusFunctionCode.READ_HOLDING_REGISTERS],
[0x04, ModbusFunctionCode.READ_INPUT_REGISTERS],
[0x05, ModbusFunctionCode.WRITE_SINGLE_COIL],
[0x06, ModbusFunctionCode.WRITE_SINGLE_REGISTER],
[0x0f, ModbusFunctionCode.WRITE_MULTIPLE_COILS],
[0x10, ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS],
[0x11, ModbusFunctionCode.REPORT_SLAVE_ID],
[0x14, ModbusFunctionCode.READ_DEVICE_COMMENT],
[0x15, ModbusFunctionCode.WRITE_DEVICE_COMMENT],
[0x2b, ModbusFunctionCode.READ_DEVICE_IDENTIFICATION],
[0x52, ModbusFunctionCode.READ_FILE_LENGTH],
[0x5a, ModbusFunctionCode.READ_FILE_CHUNK],
[0x55, ModbusFunctionCode.OPEN_FILE],
[0x57, ModbusFunctionCode.CLOSE_FILE],
[0x5c, ModbusFunctionCode.RESTART_CONTROLLER],
[0x6e, ModbusFunctionCode.GET_CONTROLLER_TIME],
[0x6f, ModbusFunctionCode.SET_CONTROLLER_TIME],
]);
private static readonly EXCEPTION_CODE_MAP = new Map<number, ModbusExceptionCode>([
[1, ModbusExceptionCode.ILLEGAL_FUNCTION],
[2, ModbusExceptionCode.ILLEGAL_DATA_ADDRESS],
[3, ModbusExceptionCode.ILLEGAL_DATA_VALUE],
[4, ModbusExceptionCode.SLAVE_DEVICE_FAILURE],
[5, ModbusExceptionCode.ACKNOWLEDGE],
[6, ModbusExceptionCode.SLAVE_DEVICE_BUSY],
[8, ModbusExceptionCode.MEMORY_PARITY_ERROR],
[10, ModbusExceptionCode.GATEWAY_PATH_UNAVAILABLE],
[11, ModbusExceptionCode.GATEWAY_TARGET_DEVICE_FAILED],
]);
constructor(
transportOrController: Transport | TransportController,
slaveId: number = 1,
options: ModbusClientOptions = {}
) {
if (!Number.isInteger(slaveId) || slaveId < 1 || slaveId > 255) {
throw new ModbusInvalidAddressError(slaveId);
}
if (transportOrController instanceof TransportController) {
this.transportController = transportOrController;
this.transport = null;
} else {
this.transport = transportOrController as ExtendedTransport;
this.transportController = null;
}
// this.transport = transport as ExtendedTransport;
this.slaveId = slaveId;
this.defaultTimeout = options.timeout ?? 2000;
this.retryCount = options.retryCount ?? 0;
this.retryDelay = options.retryDelay ?? 100;
this.echoEnabled = options.echoEnabled ?? false;
this.diagnosticsEnabled = !!options.diagnostics;
this.diagnostics = this.diagnosticsEnabled
? new Diagnostics({ loggerName: 'ModbusClient' })
: new Diagnostics({ loggerName: 'Noop' });
const algorithm = options.crcAlgorithm ?? 'crc16Modbus';
const crcFunc = crcAlgorithmMap[algorithm];
if (!crcFunc) {
throw new ModbusConfigError(`Unknown CRC algorithm: ${algorithm}`);
}
this.crcFunc = crcFunc;
this._mutex = new Mutex();
this._setAutoLoggerContext();
}
/**
* Returns the effective transport for the client
*/
private get _effectiveTransport(): ExtendedTransport | null {
if (this.transportController) {
return this.transportController.getTransportForSlave(
this.slaveId
) as ExtendedTransport | null;
}
return this.transport;
}
/**
* Enables the ModbusClient logger
* @param level - Logging level
*/
enableLogger(level: 'trace' | 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
logger.setLevel(level);
}
/**
* Disables the ModbusClient logger (sets the highest level - error)
*/
disableLogger(): void {
logger.setLevel('error');
}
/**
* Sets the context for the logger (slaveId, functionCode, etc.)
* @param context - Context for the logger
*/
setLoggerContext(context: LoggerContext): void {
logger.addGlobalContext(context);
}
/**
* Sets the logger context automatically based on the current settings.
*/
private _setAutoLoggerContext(funcCode?: number): void {
const transport = this._effectiveTransport;
const transportName = transport ? transport.constructor.name : 'Unknown';
const context: LoggerContext = {
slaveId: this.slaveId,
transport: transportName,
};
if (funcCode !== undefined) {
context.funcCode = funcCode;
}
logger.addGlobalContext(context);
}
/**
* Establishes a connection to the Modbus transport.
* Logs the connection status upon successful connection.
*/
public async connect(): Promise<void> {
const release = await this._mutex.acquire();
try {
const transport = this._effectiveTransport;
if (!transport) {
throw new Error(`No transport available for slaveId ${this.slaveId}`);
}
await transport.connect();
this._setAutoLoggerContext();
logger.info('Transport connected', { transport: transport.constructor.name });
} catch (err: unknown) {
if (err instanceof ModbusConnectionRefusedError) {
logger.error('Connection refused');
} else if (err instanceof ModbusConnectionTimeoutError) {
logger.error('Connection timeout');
} else if (err instanceof ModbusNotConnectedError) {
logger.error('Not connected');
} else if (err instanceof ModbusAlreadyConnectedError) {
logger.error('Already connected');
} else {
logger.error('Connection error', { error: err });
}
throw err;
} finally {
release();
}
}
/**
* Closes the connection to the Modbus transport.
* Logs the disconnection status upon successful disconnection.
*/
public async disconnect(): Promise<void> {
const release = await this._mutex.acquire();
try {
const transport = this._effectiveTransport;
if (!transport) {
logger.warn('No transport available to disconnect');
return;
}
await transport.disconnect();
this._setAutoLoggerContext();
logger.info('Transport disconnected', { transport: transport.constructor.name });
} catch (err: unknown) {
if (err instanceof ModbusConnectionRefusedError) {
logger.error('Connection refused during disconnect');
} else if (err instanceof ModbusConnectionTimeoutError) {
logger.error('Connection timeout during disconnect');
} else if (err instanceof ModbusNotConnectedError) {
logger.error('Not connected during disconnect');
} else {
logger.error('Disconnection error', { error: err });
}
throw err;
} finally {
release();
}
}
/**
* Converts a buffer to a hex string.
* @param buffer - The buffer to convert.
* @returns The hex string representation of the buffer.
*/
private _toHex(buffer: Uint8Array): string {
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.
*/
private _getExpectedResponseLength(pdu: Uint8Array): number | null {
if (!pdu || pdu.length === 0) return null;
const funcCode = pdu[0];
const modbusFuncCode = ModbusClient.FUNCTION_CODE_MAP.get(funcCode!);
if (!modbusFuncCode) {
if (funcCode! & 0x80) {
return 5; // slave(1) + func(1) + errorCode(1) + CRC(2)
}
return null; // Неизвестный код функции
}
switch (modbusFuncCode) {
case ModbusFunctionCode.READ_COILS:
case ModbusFunctionCode.READ_DISCRETE_INPUTS: {
if (pdu.length < 5) return null;
const bitCount = (pdu[3]! << 8) | pdu[4]!;
if (bitCount < 1 || bitCount > 2000) {
throw new ModbusInvalidQuantityError(bitCount, 1, 2000);
}
return 5 + Math.ceil(bitCount / 8); // slave(1) + func(1) + byteCount(1) + data(N) + CRC(2)
}
case ModbusFunctionCode.READ_HOLDING_REGISTERS:
case ModbusFunctionCode.READ_INPUT_REGISTERS: {
if (pdu.length < 5) return null;
const regCount = (pdu[3]! << 8) | pdu[4]!;
if (regCount < 1 || regCount > 125) {
throw new ModbusInvalidQuantityError(regCount, 1, 125);
}
return 5 + regCount * 2; // slave(1) + func(1) + byteCount(1) + data(N*2) + CRC(2)
}
case ModbusFunctionCode.WRITE_SINGLE_COIL:
case ModbusFunctionCode.WRITE_SINGLE_REGISTER:
return 8; // slave(1) + func(1) + address(2) + value(2) + CRC(2)
case ModbusFunctionCode.WRITE_MULTIPLE_COILS:
case ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS:
return 8; // slave(1) + func(1) + address(2) + quantity(2) + CRC(2)
case ModbusFunctionCode.READ_DEVICE_COMMENT:
return null;
case ModbusFunctionCode.WRITE_DEVICE_COMMENT:
return 5; // slave(1) + func(1) + channel(1) + length(1) + CRC(2)
case ModbusFunctionCode.READ_DEVICE_IDENTIFICATION: {
if (pdu.length < 4) return null;
if (pdu[2] === 0x00) return 6; // slave(1) + func(1) + interface(1) + error(1) + CRC(2)
if (pdu[2] === 0x04) return null; // Длина строки неизвестна заранее
return null; // Количество строк и их длины неизвестны заранее
}
case ModbusFunctionCode.READ_FILE_LENGTH:
return 8; // slave(1) + func(1) + length(4) + CRC(2)
case ModbusFunctionCode.OPEN_FILE:
return 8; // slave(1) + func(1) + length(4) + CRC(2)
case ModbusFunctionCode.CLOSE_FILE:
return 5; // slave(1) + func(1) + status(1) + CRC(2)
case ModbusFunctionCode.RESTART_CONTROLLER:
return 0; // Ответа не ожидается
case ModbusFunctionCode.GET_CONTROLLER_TIME:
return 10; // slave(1) + func(1) + time(6) + CRC(2)
case ModbusFunctionCode.SET_CONTROLLER_TIME:
return 8; // slave(1) + func(1) + status(2) + CRC(2)
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.
*/
private async _readPacket(
timeout: number,
requestPdu: Uint8Array | null = null
): Promise<Uint8Array> {
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 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] : undefined; // <-- Поменяй на undefined
this._setAutoLoggerContext(funcCode);
logger.debug('Received chunk:', { bytes: chunk.length, total: buffer.length });
if (buffer.length >= minPacketLength) {
try {
parsePacket(buffer, this.crcFunc);
return buffer;
} catch (err: unknown) {
if (err instanceof ModbusCRCError) {
logger.error('CRC mismatch detected');
continue;
} else if (err instanceof ModbusFramingError) {
logger.error('Framing error detected');
continue;
} else if (err instanceof ModbusParityError) {
logger.error('Parity error detected');
continue;
} else if (err instanceof ModbusNoiseError) {
logger.error('Noise error detected');
continue;
} else if (err instanceof ModbusOverrunError) {
logger.error('Overrun error detected');
continue;
} else if (err instanceof ModbusCollisionError) {
logger.error('Collision error detected');
continue;
} else if (err instanceof ModbusSyncError) {
logger.error('Sync error detected');
continue;
} else if (err instanceof ModbusFrameBoundaryError) {
logger.error('Frame boundary error detected');
continue;
} else if (err instanceof ModbusLRCError) {
logger.error('LRC error detected');
continue;
} else if (err instanceof ModbusChecksumError) {
logger.error('Checksum error detected');
continue;
} else if (err instanceof ModbusMalformedFrameError) {
logger.error('Malformed frame error detected');
continue;
} else if (err instanceof ModbusInvalidFrameLengthError) {
logger.error('Invalid frame length error detected');
continue;
} else if (err instanceof ModbusInvalidTransactionIdError) {
logger.error('Invalid transaction ID error detected');
continue;
} else if (err instanceof ModbusUnexpectedFunctionCodeError) {
logger.error('Unexpected function code error detected');
continue;
} else if (err instanceof ModbusTooManyEmptyReadsError) {
logger.error('Too many empty reads error detected');
continue;
} else if (err instanceof ModbusInterFrameTimeoutError) {
logger.error('Inter-frame timeout error detected');
continue;
} else if (err instanceof ModbusSilentIntervalError) {
logger.error('Silent interval error detected');
continue;
} else if (err instanceof ModbusResponseError) {
logger.error('Response error detected');
continue;
} else if (err instanceof ModbusBufferOverflowError) {
logger.error('Buffer overflow error detected');
continue;
} else if (err instanceof ModbusBufferUnderrunError) {
logger.error('Buffer underrun error detected');
continue;
} else if (err instanceof ModbusMemoryError) {
logger.error('Memory error detected');
continue;
} else if (err instanceof ModbusStackOverflowError) {
logger.error('Stack overflow error detected');
continue;
} else if (err instanceof 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.
*/
private async _sendRequest(
pdu: Uint8Array,
timeout: number = this.defaultTimeout,
ignoreNoResponse: boolean = false
): Promise<Uint8Array | undefined> {
const release = await this._mutex.acquire();
try {
const funcCode = pdu[0];
const funcCodeEnum = ModbusClient.FUNCTION_CODE_MAP.get(funcCode!) ?? funcCode;
const slaveId = this.slaveId;
if (this.diagnosticsEnabled) {
this.diagnostics.recordRequest(slaveId, funcCode);
this.diagnostics.recordFunctionCall(funcCode!, slaveId);
}
let lastError: unknown;
const startTime = Date.now();
for (let attempt = 0; attempt <= this.retryCount; attempt++) {
try {
const attemptStart = Date.now();
const timeLeft = timeout - (attemptStart - startTime);
if (timeLeft <= 0) throw new ModbusTimeoutError('Timeout before request');
this._setAutoLoggerContext(funcCodeEnum);
logger.debug(`Attempt #${attempt + 1} — sending request`, {
slaveId,
funcCode,
});
const packet = buildPacket(slaveId, pdu, this.crcFunc);
if (this.diagnosticsEnabled) {
this.diagnostics.recordDataSent(packet.length, slaveId, funcCode);
}
const transport = this._effectiveTransport;
if (!transport) {
throw new Error(`No transport available for slaveId ${this.slaveId}`);
}
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 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) {
const elapsed = Date.now() - startTime;
if (this.diagnosticsEnabled) {
this.diagnostics.recordSuccess(elapsed, slaveId, funcCode);
}
logger.info('Request sent, no response expected', {
slaveId,
funcCode,
responseTime: elapsed,
});
if (transport.notifyDeviceConnected) {
transport.notifyDeviceConnected(slaveId);
}
return undefined;
}
const response = await this._readPacket(timeLeft, pdu);
if (this.diagnosticsEnabled) {
this.diagnostics.recordDataReceived(response.length, slaveId, funcCode);
}
const elapsed = Date.now() - startTime;
const { slaveAddress, pdu: responsePdu } = parsePacket(response, this.crcFunc);
if (slaveAddress !== slaveId) {
throw new Error(`Slave address mismatch (expected ${slaveId}, got ${slaveAddress})`);
}
const responseFuncCode = responsePdu[0];
if ((responseFuncCode! & 0x80) !== 0) {
const exceptionCode = responsePdu[1];
const modbusExceptionCode =
ModbusClient.EXCEPTION_CODE_MAP.get(exceptionCode!) ?? exceptionCode;
const exceptionMessage =
MODBUS_EXCEPTION_MESSAGES[exceptionCode as ModbusExceptionCode] ??
`Unknown exception code: ${exceptionCode}`;
logger.warn('Modbus exception received', {
slaveId,
funcCode,
exceptionCode,
exceptionMessage,
responseTime: elapsed,
});
if (transport.notifyDeviceDisconnected) {
transport.notifyDeviceDisconnected(slaveId, 'ModbusException', exceptionMessage);
}
throw new ModbusExceptionError(responseFuncCode! & 0x7f, modbusExceptionCode!);
}
if (this.diagnosticsEnabled) {
this.diagnostics.recordSuccess(elapsed, slaveId, funcCode);
}
logger.info('Response received', {
slaveId,
funcCode,
responseTime: elapsed,
});
if (transport.notifyDeviceConnected) {
transport.notifyDeviceConnected(slaveId);
}
return responsePdu;
} catch (err: unknown) {
const elapsed = Date.now() - startTime;
const isFlushedError = err instanceof ModbusFlushError;
const errorCode =
err instanceof Error && err.message.toLowerCase().includes('timeout')
? 'timeout'
: err instanceof Error && err.message.toLowerCase().includes('crc')
? 'crc'
: err instanceof ModbusExceptionError
? 'modbus-exception'
: null;
const isCriticalError =
err instanceof ModbusTimeoutError ||
err instanceof ModbusConnectionTimeoutError ||
err instanceof ModbusNotConnectedError ||
err instanceof ModbusConnectionRefusedError;
const transport = this._effectiveTransport;
if (isCriticalError && transport && transport.notifyDeviceDisconnected) {
const errorType = err.constructor.name;
const errorMessage = err instanceof Error ? err.message : String(err);
transport.notifyDeviceDisconnected(slaveId, errorType, errorMessage);
if (transport.flush) {
try {
await transport.flush();
logger.debug('Transport flushed after critical error', { slaveId });
} catch (flushErr) {
logger.warn('Failed to flush transport after error', {
slaveId,
flushError: flushErr,
});
}
}
}
if (this.diagnosticsEnabled) {
this.diagnostics.recordError(err instanceof Error ? err : new Error(String(err)), {
code: errorCode,
responseTimeMs: elapsed,
slaveId,
funcCode,
exceptionCode: err instanceof ModbusExceptionError ? err.exceptionCode : null,
});
}
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,
exceptionCode: err instanceof 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,
});
if (this.diagnosticsEnabled) {
this.diagnostics.recordSuccess(elapsed, slaveId, funcCode);
}
if (transport && transport.notifyDeviceConnected) {
transport.notifyDeviceConnected(slaveId);
}
return undefined;
}
if (attempt < this.retryCount) {
if (this.diagnosticsEnabled) {
this.diagnostics.recordRetry(1, slaveId, funcCode);
}
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 (!isCriticalError && transport && transport.notifyDeviceDisconnected) {
const errorType = err!.constructor.name;
const errorMessage = err instanceof Error ? err.message : String(err);
transport.notifyDeviceDisconnected(slaveId, errorType, errorMessage);
}
if (transport && 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.
*/
private _convertRegisters<T extends RegisterType>(
registers: number[],
type: T = RegisterType.UINT16 as T
): ConvertedRegisters<T> {
if (!registers || !Array.isArray(registers)) {
throw new ModbusDataConversionError(registers, 'non-empty array');
}
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: DataViewMethod32, littleEndian: boolean = false): number[] => {
const result: number[] = [];
for (let i = 0; i < registers.length - 1; i += 2) {
result.push(view[method](i * 2, littleEndian));
}
return result;
};
const read64 = (
method: DataViewMethod64,
littleEndian: boolean = false
): bigint[] | number[] => {
if (method === 'getFloat64') {
const result: number[] = [];
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));
}
result.push(tempView.getFloat64(0, littleEndian));
}
return result;
}
const result: bigint[] = [];
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: number, mode: SwapMode): DataView => {
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: number[];
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: DataViewMethod32, mode: SwapMode): number[] => {
const result: number[] = [];
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 RegisterType.UINT16:
return registers as ConvertedRegisters<T>;
case RegisterType.INT16:
return registers.map((_, i) => view.getInt16(i * 2, false)) as ConvertedRegisters<T>;
case RegisterType.UINT32:
return read32('getUint32') as ConvertedRegisters<T>;
case RegisterType.INT32:
return read32('getInt32') as ConvertedRegisters<T>;
case RegisterType.FLOAT:
return read32('getFloat32') as ConvertedRegisters<T>;
case RegisterType.UINT32_LE:
return read32('getUint32', true) as ConvertedRegisters<T>;
case RegisterType.INT32_LE:
return read32('getInt32', true) as ConvertedRegisters<T>;
case RegisterType.FLOAT_LE:
return read32('getFloat32', true) as ConvertedRegisters<T>;
case RegisterType.UINT32_SW:
return read32Swapped('getUint32', 'sw') as ConvertedRegisters<T>;
case RegisterType.INT32_SW:
return read32Swapped('getInt32', 'sw') as ConvertedRegisters<T>;
case RegisterType.FLOAT_SW:
return read32Swapped('getFloat32', 'sw') as ConvertedRegisters<T>;
case RegisterType.UINT32_SB:
return read32Swapped('getUint32', 'sb') as ConvertedRegisters<T>;
case RegisterType.INT32_SB:
return read32Swapped('getInt32', 'sb') as ConvertedRegisters<T>;
case RegisterType.FLOAT_SB:
return read32Swapped('getFloat32', 'sb') as ConvertedRegisters<T>;
case RegisterType.UINT32_SBW:
return read32Swapped('getUint32', 'sbw') as ConvertedRegisters<T>;
case RegisterType.INT32_SBW:
return read32Swapped('getInt32', 'sbw') as ConvertedRegisters<T>;
case RegisterType.FLOAT_SBW:
return read32Swapped('getFloat32', 'sbw') as ConvertedRegisters<T>;
case RegisterType.UINT32_LE_SW:
return read32Swapped('getUint32', 'le_sw') as ConvertedRegisters<T>;
case RegisterType.INT32_LE_SW:
return read32Swapped('getInt32', 'le_sw') as ConvertedRegisters<T>;
case RegisterType.FLOAT_LE_SW:
return read32Swapped('getFloat32', 'le_sw') as ConvertedRegisters<T>;
case RegisterType.UINT32_LE_SB:
return read32Swapped('getUint32', 'le_sb') as ConvertedRegisters<T>;
case RegisterType.INT32_LE_SB:
return read32Swapped('getInt32', 'le_sb') as ConvertedRegisters<T>;
case RegisterType.FLOAT_LE_SB:
return read32Swapped('getFloat32', 'le_sb') as ConvertedRegisters<T>;
case RegisterType.UINT32_LE_SBW:
return read32Swapped('getUint32', 'le_sbw') as ConvertedRegisters<T>;
case RegisterType.INT32_LE_SBW:
return read32Swapped('getInt32', 'le_sbw') as ConvertedRegisters<T>;
case RegisterType.FLOAT_LE_SBW:
return read32Swapped('getFloat32', 'le_sbw') as ConvertedRegisters<T>;
case RegisterType.UINT64:
return read64('getUint64') as ConvertedRegisters<T>;
case RegisterType.INT64:
return read64('getInt64') as ConvertedRegisters<T>;
case RegisterType.DOUBLE:
return read64('getFloat64') as ConvertedRegisters<T>;
case RegisterType.UINT64_LE:
return read64('getUint64', true) as ConvertedRegisters<T>;
case RegisterType.INT64_LE:
return read64('getInt64', true) as ConvertedRegisters<T>;
case RegisterType.DOUBLE_LE:
return read64('getFloat64', true) as ConvertedRegisters<T>;
case RegisterType.HEX:
return registers.map(r =>
r.toString(16).toUpperCase().padStart(4, '0')
) as ConvertedRegisters<T>;
case RegisterType.STRING: {
let str = '';
for (let i = 0; i < registers.length; i++) {
const high = (registers[i]! >> 8) & 0xff;
const low = registers[i]! & 0xff;
if (high !== 0) str += String.fromCharCode(high);
if (low !== 0) str += String.fromCharCode(low);
}
return [str] as ConvertedRegisters<T>;
}
case RegisterType.BOOL:
return registers.map(r => r !== 0) as ConvertedRegisters<T>;
case RegisterType.BINARY:
return registers.map(r =>
r
.toString(2)
.padStart(16, '0')
.split('')
.map(b => b === '1')
) as ConvertedRegisters<T>;
case RegisterType.BCD:
return registers.map(r => {
const high = (r >> 8) & 0xff;
const low = r & 0xff;
return ((high >> 4) * 10 + (high & 0x0f)) * 100 + (low >> 4) * 10 + (low & 0x0f);
}) as ConvertedRegisters<T>;
default:
throw new ModbusDataConversionError(type, 'supported type');
}
}
/**
* Reads holding registers from the Modbus device.
* @param startAddress - The starting address of the registers to read (0–65535).
* @param quantity - The number of registers to read (1–125 for Modbus standard).
* @param options - The options for the read operation.
* @returns The converted registers.
*/
public async readHoldingRegisters<T extends RegisterType>(
startAddress: number,
quantity: number,
options: ConvertRegisterOptions = {}
): Promise<ConvertedRegisters<T>> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 125) {
throw new ModbusInvalidQuantityError(quantity, 1, 125);
}
const pdu = buildReadHoldingRegistersRequest(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu);
if (!responsePdu) {
throw new Error('No response received');
}
const registers = parseReadHoldingRegistersResponse(responsePdu);
const type = options.type ?? RegisterType.UINT16;
return this._convertRegisters(registers, type) as ConvertedRegisters<T>;
}
/**
* Reads input registers from the Modbus device.
* @param startAddress - The starting address of the registers to read (0–65535).
* @param quantity - The number of registers to read (1–125 for Modbus standard).
* @param options - The options for the read operation.
* @returns The converted registers.
*/
public async readInputRegisters<T extends RegisterType>(
startAddress: number,
quantity: number,
options: ConvertRegisterOptions = {}
): Promise<ConvertedRegisters<T>> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 125) {
throw new ModbusInvalidQuantityError(quantity, 1, 125);
}
const pdu = buildReadInputRegistersRequest(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu);
if (!responsePdu) {
throw new Error('No response received');
}
const registers = parseReadInputRegistersResponse(responsePdu);
const type = options.type ?? RegisterType.UINT16;
return this._convertRegisters(registers, type) as ConvertedRegisters<T>;
}
/**
* Writes a single register to the Modbus device.
* @param address - The address of the register to write (0–65535).
* @param value - The value to write to the register (0–65535).
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async writeSingleRegister(
address: number,
value: number,
timeout?: number
): Promise<WriteSingleRegisterResponse> {
if (!Number.isInteger(address) || address < 0 || address > 65535) {
throw new ModbusInvalidAddressError(address);
}
if (!Number.isInteger(value) || value < 0 || value > 65535) {
throw new ModbusIllegalDataValueError(value, 'integer between 0 and 65535');
}
const pdu = buildWriteSingleRegisterRequest(address, value);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseWriteSingleRegisterResponse(responsePdu);
}
/**
* Writes multiple registers to the Modbus device.
* @param startAddress - The starting address of the registers to write (0–65535).
* @param values - The values to write to the registers (each 0–65535).
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async writeMultipleRegisters(
startAddress: number,
values: number[],
timeout?: number
): Promise<WriteMultipleRegistersResponse> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Array.isArray(values) || values.length < 1 || values.length > 123) {
throw new 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 ModbusIllegalDataValueError(invalidValue!, 'integer between 0 and 65535');
}
const pdu = buildWriteMultipleRegistersRequest(startAddress, values);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseWriteMultipleRegistersResponse(responsePdu);
}
/**
* Reads coils from the Modbus device.
* @param startAddress - The starting address of the coils to read (0–65535).
* @param quantity - The number of coils to read (1–2000 for Modbus standard).
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async readCoils(
startAddress: number,
quantity: number,
timeout?: number
): Promise<ReadCoilsResponse> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 2000) {
throw new ModbusInvalidQuantityError(quantity, 1, 2000);
}
const pdu = buildReadCoilsRequest(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseReadCoilsResponse(responsePdu);
}
/**
* Reads discrete inputs from the Modbus device.
* @param startAddress - The starting address of the discrete inputs to read (0–65535).
* @param quantity - The number of discrete inputs to read (1–2000 for Modbus standard).
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async readDiscreteInputs(
startAddress: number,
quantity: number,
timeout?: number
): Promise<ReadDiscreteInputsResponse> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 2000) {
throw new ModbusInvalidQuantityError(quantity, 1, 2000);
}
const pdu = buildReadDiscreteInputsRequest(startAddress, quantity);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseReadDiscreteInputsResponse(responsePdu);
}
/**
* Writes a single coil to the Modbus device.
* @param address - The address of the coil to write (0–65535).
* @param value - The value to write to the coil (boolean or 0/1).
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async writeSingleCoil(
address: number,
value: boolean | number,
timeout?: number
): Promise<WriteSingleCoilResponse> {
if (!Number.isInteger(address) || address < 0 || address > 65535) {
throw new ModbusInvalidAddressError(address);
}
if (typeof value === 'number' && value !== 0 && value !== 1) {
throw new ModbusIllegalDataValueError(value, 'boolean or 0/1');
}
const pdu = buildWriteSingleCoilRequest(address, value);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseWriteSingleCoilResponse(responsePdu);
}
/**
* Writes multiple coils to the Modbus device.
* @param startAddress - The starting address of the coils to write (0–65535).
* @param values - The values to write to the coils.
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async writeMultipleCoils(
startAddress: number,
values: boolean[],
timeout?: number
): Promise<WriteMultipleCoilsResponse> {
if (!Number.isInteger(startAddress) || startAddress < 0 || startAddress > 65535) {
throw new ModbusInvalidAddressError(startAddress);
}
if (!Array.isArray(values) || values.length < 1 || values.length > 1968) {
throw new ModbusInvalidQuantityError(values.length, 1, 1968);
}
const pdu = buildWriteMultipleCoilsRequest(startAddress, values);
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseWriteMultipleCoilsResponse(responsePdu);
}
/**
* Reports the slave ID of the Modbus device.
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async reportSlaveId(timeout?: number): Promise<ReportSlaveIdResponse> {
const pdu = buildReportSlaveIdRequest();
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseReportSlaveIdResponse(responsePdu);
}
/**
* Reads the device identification from the Modbus device.
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async readDeviceIdentification(
timeout?: number
): Promise<ReadDeviceIdentificationResponse> {
const originalSlaveId = this.slaveId;
try {
const pdu = buildReadDeviceIdentificationRequest();
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu) {
throw new Error('No response received');
}
return parseReadDeviceIdentificationResponse(responsePdu);
} finally {
this.slaveId = originalSlaveId;
}
}
/**
* Reads the file length from the Modbus device.
* @param timeout - The timeout in milliseconds.
* @returns The response from the Modbus device.
*/
public async readFileLength(timeout?: number): Promise<ReadFileLengthResponse> {
const pdu = buildReadFileLengthRequest('');
const responsePdu = await this._sendRequest(pdu, timeout);
if (!responsePdu)