modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
442 lines (380 loc) • 17.8 kB
JavaScript
// slave-emulator/SlaveEmulator.js
const logger = require('../logger.js');
const { FUNCTION_CODES } = require('../constants/constants.js');
const ModbusExceptionError = require('../errors.js');
const crc16Modbus = require('../utils/crc.js');
class SlaveEmulator {
constructor(slaveAddress = 1) {
this.slaveAddress = slaveAddress;
this.coils = new Map();
this.discreteInputs = new Map();
this.holdingRegisters = new Map();
this.inputRegisters = new Map();
this.exceptions = new Map();
this.log = logger.createLogger('SlaveEmulator');
this.connected = false;
this._infinityTasks = new Map()
}
async connect() {
this.log.info('Connecting to emulator...');
this.connected = true;
this.log.info('Connected');
}
async disconnect() {
this.log.info('Disconnecting from emulator...');
this.connected = false;
this.log.info('Disconnected');
}
infinityChange({ typeRegister, register, range, interval }) {
const key = `${typeRegister}:${register}`
if (this._infinityTasks.has(key)) {
clearInterval(this._infinityTasks.get(key))
}
const [min, max] = range
const setter = {
Holding: this.setHoldingRegister.bind(this),
Input: this.setInputRegister.bind(this),
Coil: this.setCoil.bind(this),
Discrete: this.setDiscreteInput.bind(this),
}[typeRegister]
if (!setter) {
throw new Error(`Invalid register type: ${typeRegister}`)
}
const intervalId = setInterval(() => {
const value = typeRegister === 'Holding' || typeRegister === 'Input'
? Math.floor(Math.random() * (max - min + 1)) + min
: Math.random() < 0.5
setter(register, value)
}, interval)
this._infinityTasks.set(key, intervalId)
}
stopInfinityChange({ typeRegister, register }) {
const key = `${typeRegister}:${register}`
if (this._infinityTasks.has(key)) {
clearInterval(this._infinityTasks.get(key))
this._infinityTasks.delete(key)
}
}
setException(functionCode, address, exceptionCode) {
this.exceptions.set(`${functionCode}_${address}`, exceptionCode);
this.log.info(`Exception set: functionCode=0x${functionCode.toString(16)}, address=${address}, exceptionCode=0x${exceptionCode.toString(16)}`);
}
_checkException(functionCode, address) {
const key = `${functionCode}_${address}`;
if (this.exceptions.has(key)) {
const exCode = this.exceptions.get(key);
this.log.warn(`Throwing exception for function 0x${functionCode.toString(16)} at address ${address}: code 0x${exCode.toString(16)}`);
throw new ModbusExceptionError(functionCode, exCode);
}
}
addRegisters(definitions) {
if (definitions.holding) {
for (const { start, value } of definitions.holding) {
this.setHoldingRegister(start, value);
}
}
if (definitions.input) {
for (const { start, value } of definitions.input) {
this.setInputRegister(start, value);
}
}
if (definitions.coils) {
for (const { start, value } of definitions.coils) {
this.setCoil(start, value);
}
}
if (definitions.discrete) {
for (const { start, value } of definitions.discrete) {
this.setDiscreteInput(start, value);
}
}
console.log('Registers added:', definitions);
}
setCoil(address, value) {
this.coils.set(address, !!value);
this.log.debug('Coil set:', address, !!value);
}
getCoil(address) {
return this.coils.get(address) || false;
}
readCoils(startAddress, quantity) {
this.log.info(`readCoils: start=${startAddress}, quantity=${quantity}`);
for (let addr = startAddress; addr < startAddress + quantity; addr++) {
this._checkException(FUNCTION_CODES.READ_COILS, addr);
}
const result = [];
for (let i = 0; i < quantity; i++) {
result.push(this.getCoil(startAddress + i));
}
return result;
}
writeSingleCoil(address, value) {
this._checkException(FUNCTION_CODES.WRITE_SINGLE_COIL, address);
this.setCoil(address, value);
this.log.info(`writeSingleCoil: address=${address}, value=${!!value}`);
}
writeMultipleCoils(startAddress, values) {
values.forEach((val, idx) => {
this._checkException(FUNCTION_CODES.WRITE_MULTIPLE_COILS, startAddress + idx);
});
values.forEach((val, idx) => {
this.setCoil(startAddress + idx, val);
});
this.log.info(`writeMultipleCoils: startAddress=${startAddress}, values=${values}`);
}
setDiscreteInput(address, value) {
this.discreteInputs.set(address, !!value);
this.log.debug('Discrete Input set:', address, !!value);
}
getDiscreteInput(address) {
return this.discreteInputs.get(address) || false;
}
readDiscreteInputs(startAddress, quantity) {
this.log.info(`readDiscreteInputs: start=${startAddress}, quantity=${quantity}`);
for (let addr = startAddress; addr < startAddress + quantity; addr++) {
this._checkException(FUNCTION_CODES.READ_DISCRETE_INPUTS, addr);
}
const result = [];
for (let i = 0; i < quantity; i++) {
result.push(this.getDiscreteInput(startAddress + i));
}
return result;
}
setHoldingRegister(address, value) {
this.holdingRegisters.set(address, value & 0xFFFF);
this.log.debug('Holding Register set:', address, value & 0xFFFF);
}
getHoldingRegister(address) {
return this.holdingRegisters.get(address) || 0;
}
readHoldingRegisters(startAddress, quantity) {
this.log.info(`readHoldingRegisters: start=${startAddress}, quantity=${quantity}`);
for (let addr = startAddress; addr < startAddress + quantity; addr++) {
this._checkException(FUNCTION_CODES.READ_HOLDING_REGISTERS, addr);
}
const result = [];
for (let i = 0; i < quantity; i++) {
result.push(this.getHoldingRegister(startAddress + i));
}
return result;
}
writeSingleRegister(address, value) {
this._checkException(FUNCTION_CODES.WRITE_SINGLE_REGISTER, address);
this.setHoldingRegister(address, value);
this.log.info(`writeSingleRegister: address=${address}, value=${value}`);
}
writeMultipleRegisters(startAddress, values) {
values.forEach((val, idx) => {
this._checkException(FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS, startAddress + idx);
});
values.forEach((val, idx) => {
this.setHoldingRegister(startAddress + idx, val);
});
this.log.info(`writeMultipleRegisters: startAddress=${startAddress}, values=${values}`);
}
setInputRegister(address, value) {
this.inputRegisters.set(address, value & 0xFFFF);
this.log.debug('Input Register set:', address, value & 0xFFFF);
}
getInputRegister(address) {
return this.inputRegisters.get(address) || 0;
}
readInputRegisters(startAddress, quantity) {
this.log.info(`readInputRegisters: start=${startAddress}, quantity=${quantity}`);
for (let addr = startAddress; addr < startAddress + quantity; addr++) {
this._checkException(FUNCTION_CODES.READ_INPUT_REGISTERS, addr);
}
const result = [];
for (let i = 0; i < quantity; i++) {
result.push(this.getInputRegister(startAddress + i));
}
return result;
}
// --- Прямые методы (без RTU) ---
readHolding(start, quantity) {
const result = [];
for (let i = 0; i < quantity; i++) {
const addr = start + i;
this._checkException(FUNCTION_CODES.READ_HOLDING_REGISTERS, addr);
result.push(this.getHoldingRegister(addr));
}
return result;
}
readInput(start, quantity) {
const result = [];
for (let i = 0; i < quantity; i++) {
const addr = start + i;
this._checkException(FUNCTION_CODES.READ_INPUT_REGISTERS, addr);
result.push(this.getInputRegister(addr));
}
return result;
}
// --- Modbus RTU Frame handler ---
handleRequest(buffer) {
if (!this.connected) {
this.log.warn('Received request but emulator not connected');
return null;
}
if (!(buffer instanceof Uint8Array)) {
throw new Error('Input buffer must be Uint8Array or Buffer');
}
if (buffer.length < 5) {
throw new Error('Invalid Modbus RTU frame: too short');
}
const crcReceived = buffer[buffer.length - 2] | (buffer[buffer.length - 1] << 8);
const crcCalculated = crc16Modbus(buffer.subarray(0, buffer.length - 2));
if (crcReceived !== crcCalculated) {
this.log.warn(`CRC mismatch: received=0x${crcReceived.toString(16)}, calculated=0x${crcCalculated.toString(16)}`);
return null;
}
const slaveAddr = buffer[0];
if (slaveAddr !== this.slaveAddress && slaveAddr !== 0) {
this.log.debug(`Frame for slave ${slaveAddr} ignored (this slave: ${this.slaveAddress})`);
return null;
}
const functionCode = buffer[1];
const data = buffer.subarray(2, buffer.length - 2);
this.log.info(`Request: slave=${slaveAddr}, function=0x${functionCode.toString(16)}, data=${Buffer.from(data).toString('hex')}`);
try {
let responseData;
switch (functionCode) {
case FUNCTION_CODES.READ_COILS: {
if (data.length !== 4) throw new Error('Invalid data length for Read Coils');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const coils = this.readCoils(startAddr, qty);
const byteCount = Math.ceil(qty / 8);
const resp = new Uint8Array(1 + byteCount);
resp[0] = byteCount;
for (let i = 0; i < qty; i++) {
if (coils[i]) {
resp[1 + Math.floor(i / 8)] |= 1 << (i % 8);
}
}
responseData = resp;
break;
}
case FUNCTION_CODES.READ_DISCRETE_INPUTS: {
if (data.length !== 4) throw new Error('Invalid data length for Read Discrete Inputs');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const inputs = this.readDiscreteInputs(startAddr, qty);
const byteCount = Math.ceil(qty / 8);
const resp = new Uint8Array(1 + byteCount);
resp[0] = byteCount;
for (let i = 0; i < qty; i++) {
if (inputs[i]) {
resp[1 + Math.floor(i / 8)] |= 1 << (i % 8);
}
}
responseData = resp;
break;
}
case FUNCTION_CODES.READ_HOLDING_REGISTERS: {
if (data.length !== 4) throw new Error('Invalid data length for Read Holding Registers');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const regs = this.readHoldingRegisters(startAddr, qty);
const byteCount = qty * 2;
const resp = new Uint8Array(1 + byteCount);
resp[0] = byteCount;
for (let i = 0; i < qty; i++) {
resp[1 + i * 2] = regs[i] >> 8;
resp[2 + i * 2] = regs[i] & 0xFF;
}
responseData = resp;
break;
}
case FUNCTION_CODES.READ_INPUT_REGISTERS: {
if (data.length !== 4) throw new Error('Invalid data length for Read Input Registers');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const regs = this.readInputRegisters(startAddr, qty);
const byteCount = qty * 2;
const resp = new Uint8Array(1 + byteCount);
resp[0] = byteCount;
for (let i = 0; i < qty; i++) {
resp[1 + i * 2] = regs[i] >> 8;
resp[2 + i * 2] = regs[i] & 0xFF;
}
responseData = resp;
break;
}
case FUNCTION_CODES.WRITE_SINGLE_COIL: {
if (data.length !== 4) throw new Error('Invalid data length for Write Single Coil');
const addr = (data[0] << 8) | data[1];
const val = (data[2] << 8) | data[3];
if (val !== 0x0000 && val !== 0xFF00) throw new Error('Invalid coil value');
this.writeSingleCoil(addr, val === 0xFF00);
responseData = data;
break;
}
case FUNCTION_CODES.WRITE_SINGLE_REGISTER: {
if (data.length !== 4) throw new Error('Invalid data length for Write Single Register');
const addr = (data[0] << 8) | data[1];
const val = (data[2] << 8) | data[3];
this.writeSingleRegister(addr, val);
responseData = data;
break;
}
case FUNCTION_CODES.WRITE_MULTIPLE_COILS: {
if (data.length < 5) throw new Error('Invalid data length for Write Multiple Coils');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const byteCount = data[4];
if (byteCount !== data.length - 5) throw new Error('Byte count mismatch');
const coilValues = [];
for (let i = 0; i < qty; i++) {
const byteIndex = 5 + Math.floor(i / 8);
const bitIndex = i % 8;
coilValues.push((data[byteIndex] & (1 << bitIndex)) !== 0);
}
this.writeMultipleCoils(startAddr, coilValues);
responseData = data.subarray(0, 4);
break;
}
case FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS: {
if (data.length < 5) throw new Error('Invalid data length for Write Multiple Registers');
const startAddr = (data[0] << 8) | data[1];
const qty = (data[2] << 8) | data[3];
const byteCount = data[4];
if (byteCount !== qty * 2) throw new Error('Byte count mismatch');
const regValues = [];
for (let i = 0; i < qty; i++) {
regValues.push((data[5 + i * 2] << 8) | data[6 + i * 2]);
}
this.writeMultipleRegisters(startAddr, regValues);
responseData = data.subarray(0, 4);
break;
}
default:
this.log.warn(`Unsupported function code 0x${functionCode.toString(16)}`);
throw new ModbusExceptionError(functionCode, 0x01);
}
const respBuf = new Uint8Array(2 + responseData.length + 2);
respBuf[0] = this.slaveAddress;
respBuf[1] = functionCode;
respBuf.set(responseData, 2);
const crc = crc16Modbus(respBuf.subarray(0, respBuf.length - 2));
respBuf[respBuf.length - 2] = crc & 0xFF;
respBuf[respBuf.length - 1] = crc >> 8;
this.log.info(`Response: ${Buffer.from(respBuf).toString('hex')}`);
return respBuf;
} catch (err) {
if (err instanceof ModbusExceptionError) {
const excBuf = new Uint8Array(5);
excBuf[0] = this.slaveAddress;
excBuf[1] = functionCode | 0x80;
excBuf[2] = err.exceptionCode;
const crc = crc16Modbus(excBuf.subarray(0, 3));
excBuf[3] = crc & 0xFF;
excBuf[4] = crc >> 8;
this.log.warn(`Exception response: ${Buffer.from(excBuf).toString('hex')}`);
return excBuf;
}
this.log.error(`Unexpected error in handleRequest: ${err.message}`);
return null;
}
}
}
module.exports = SlaveEmulator