const { padHex } = require('../../utils/base-utils.js') const { bytesToWords } = require('../../utils/binary-utils.js') const { BYTE_ORDER_LOW, appendCrc16Modbus, hasValidCrc16Modbus } = require('../../utils/crc.js') const settingsService = require('../../store/settings-store.js') const transport = require('../../transport/ble-core.js') const { addCoilReadValues, addWordReadValues } = require('../../utils/register-value-utils.js') const PROTOCOL_NAME = 'modbus-rtu' const MODBUS_CRC_OPTIONS = { byteOrder: BYTE_ORDER_LOW } const MAX_MODBUS_DMA_BYTES = 64 const MODBUS_READ_RESPONSE_OVERHEAD = 5 const MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD = 9 const MAX_READ_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) / 2) const MAX_READ_COIL_QUANTITY = (MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) * 8 const MAX_WRITE_MULTIPLE_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2) const UNLIMITED_FRAME_BYTES = 0 const MODBUS_EXCEPTION_MESSAGES = { 0x01: '非法功能', 0x02: '非法数据地址', 0x03: '非法数据值', 0x04: '从站设备故障', 0x05: '确认', 0x06: '从站设备忙', 0x08: '存储奇偶性错误', 0x0A: '网关路径不可用', 0x0B: '网关目标设备响应失败' } function toByte(value, label) { if (!Number.isInteger(value) || value < 0 || value > 0xFF) { throw new Error(`${label}必须在 0x00 至 0xFF 之间`) } return value } function toWord(value, label) { if (!Number.isInteger(value) || value < 0 || value > 0xFFFF) { throw new Error(`${label}必须在 0x0000 至 0xFFFF 之间`) } return value } function splitWord(value) { return [(value >> 8) & 0xFF, value & 0xFF] } function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_BYTES) { const numberValue = Number(maxFrameBytes) if (Number.isFinite(numberValue) && Math.round(numberValue) === UNLIMITED_FRAME_BYTES) return UNLIMITED_FRAME_BYTES if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue) return MAX_MODBUS_DMA_BYTES } function getMaxReadQuantity(functionCode, maxFrameBytes = MAX_MODBUS_DMA_BYTES) { const frameBytes = normalizeMaxFrameBytes(maxFrameBytes) if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF const dataBytes = frameBytes - MODBUS_READ_RESPONSE_OVERHEAD if (dataBytes <= 0) return 0 if (functionCode === 0x01 || functionCode === 0x02) return dataBytes * 8 if (functionCode === 0x03 || functionCode === 0x04) return Math.floor(dataBytes / 2) return 0 } function getMaxWriteMultipleRegisterQuantity(maxFrameBytes = MAX_MODBUS_DMA_BYTES) { const frameBytes = normalizeMaxFrameBytes(maxFrameBytes) if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF return Math.max(0, Math.floor((frameBytes - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2)) } function buildReadFrame(slaveAddress, functionCode, address, quantity, options = {}) { const slave = toByte(slaveAddress, '从站地址') const command = toByte(functionCode, '功能码') const startAddress = toWord(address, '寄存器地址') const registerQuantity = toWord(quantity, '读取数量') const maxQuantity = getMaxReadQuantity(command, options.maxFrameBytes) if ([0x01, 0x02, 0x03, 0x04].indexOf(command) < 0) { throw new Error('当前功能码不是读取命令') } if (registerQuantity === 0) { throw new Error('读取数量必须大于 0') } if ([0x03, 0x04].indexOf(command) >= 0 && maxQuantity > 0 && registerQuantity > maxQuantity) { throw new Error(`单帧最多读取 ${maxQuantity} 个寄存器`) } if ((command === 0x01 || command === 0x02) && maxQuantity > 0 && registerQuantity > maxQuantity) { throw new Error(`单帧最多读取 ${maxQuantity} 个位状态`) } return appendCrc16Modbus( [slave, command].concat(splitWord(startAddress), splitWord(registerQuantity)), MODBUS_CRC_OPTIONS ) } function buildWriteSingleCoilFrame(slaveAddress, address, checked) { const slave = toByte(slaveAddress, '从站地址') const startAddress = toWord(address, '线圈地址') const outputValue = checked ? 0xFF00 : 0x0000 return appendCrc16Modbus( [slave, 0x05].concat(splitWord(startAddress), splitWord(outputValue)), MODBUS_CRC_OPTIONS ) } function buildWriteSingleRegisterFrame(slaveAddress, address, value) { const slave = toByte(slaveAddress, '从站地址') const startAddress = toWord(address, '寄存器地址') const registerValue = toWord(value, '写入值') return appendCrc16Modbus( [slave, 0x06].concat(splitWord(startAddress), splitWord(registerValue)), MODBUS_CRC_OPTIONS ) } function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options = {}) { const slave = toByte(slaveAddress, '从站地址') const startAddress = toWord(address, '寄存器地址') const maxQuantity = getMaxWriteMultipleRegisterQuantity(options.maxFrameBytes) if (!Array.isArray(values) || values.length === 0) { throw new Error('请输入至少一个寄存器写入值') } if (maxQuantity > 0 && values.length > maxQuantity) { throw new Error(`单帧最多写入 ${maxQuantity} 个寄存器`) } const registerBytes = values.reduce((result, value) => { return result.concat(splitWord(toWord(value, '写入值'))) }, []) return appendCrc16Modbus( [slave, 0x10] .concat(splitWord(startAddress), splitWord(values.length), [registerBytes.length], registerBytes), MODBUS_CRC_OPTIONS ) } function formatHex(bytes) { return bytes.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ') } function getReadResponseByteLength(functionCode, quantity) { if (functionCode === 0x01 || functionCode === 0x02) return MODBUS_READ_RESPONSE_OVERHEAD + Math.ceil(Number(quantity || 0) / 8) if (functionCode === 0x03 || functionCode === 0x04) return MODBUS_READ_RESPONSE_OVERHEAD + Number(quantity || 0) * 2 return 0 } function parseModbusResponse(bytes) { if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null const slaveAddress = bytes[0] const functionCode = bytes[1] if (functionCode & 0x80) { return { exceptionCode: bytes[2], functionCode, isException: true, protocol: PROTOCOL_NAME, slaveAddress, sourceFunctionCode: functionCode & 0x7F } } if (functionCode === 0x01 || functionCode === 0x02) { const byteCount = bytes[2] const dataEnd = 3 + byteCount if (bytes.length < dataEnd + 2) return null return { byteCount, dataBytes: bytes.slice(3, dataEnd), functionCode, isException: false, protocol: PROTOCOL_NAME, slaveAddress } } if (functionCode === 0x03 || functionCode === 0x04) { const byteCount = bytes[2] const dataEnd = 3 + byteCount if (bytes.length < dataEnd + 2) return null return { byteCount, dataBytes: bytes.slice(3, dataEnd), functionCode, isException: false, protocol: PROTOCOL_NAME, slaveAddress, words: bytesToWords(bytes.slice(3, dataEnd)) } } if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) { return { address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF, functionCode, isException: false, protocol: PROTOCOL_NAME, quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF, slaveAddress } } return { functionCode, isException: false, protocol: PROTOCOL_NAME, slaveAddress } } function parseModbusRequest(bytes) { if (!Array.isArray(bytes) || bytes.length < 4 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null const slaveAddress = bytes[0] const functionCode = bytes[1] if (bytes.length < 6) return null const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF let quantity = 1 let value if (functionCode === 0x01 || functionCode === 0x02 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) { quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF } if (functionCode === 0x05 || functionCode === 0x06) { value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF } return { address, functionCode, kind: 'raw-hex', protocol: PROTOCOL_NAME, quantity, value, slaveAddress } } function getExpectedResponseLength(expected, responseFunctionCode, responseBytes = []) { if (!expected) return 0 if (responseFunctionCode === (expected.functionCode | 0x80)) { return 5 } if (responseFunctionCode === 0x01 || responseFunctionCode === 0x02) { if (responseBytes.length < 3) return 0 return 3 + Number(responseBytes[2] || 0) + 2 } if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) { if (responseBytes.length < 3) return 0 return 3 + Number(responseBytes[2] || 0) + 2 } if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) { return 8 } return 0 } function isExpectedResponse(response, expected) { if (response.functionCode === 0x01 || response.functionCode === 0x02) { return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8) } if (response.functionCode === 0x03 || response.functionCode === 0x04) { return Array.isArray(response.words) && response.words.length >= expected.quantity } if (response.functionCode === 0x10) { return response.address === expected.address && response.quantityOrValue === expected.quantity } if (response.functionCode === 0x05 || response.functionCode === 0x06) { if (response.address !== expected.address) return false if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value return true } return true } function getExceptionText(code) { return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常' } function formatExceptionMessage(response) { const sourceFunctionCode = response && response.sourceFunctionCode const exceptionCode = response && response.exceptionCode const exceptionText = getExceptionText(exceptionCode) return `设备返回异常帧:功能码 0x${padHex(sourceFunctionCode, 2)},异常码 0x${padHex(exceptionCode, 2)}(${exceptionText})` } function getReadBufferHint(expected) { return expected ? getReadResponseByteLength(expected.functionCode, expected.quantity) : 0 } function alignResponseBuffer(buffer, expected) { if (!Array.isArray(buffer) || !buffer.length || !expected) return const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80] let matchIndex = -1 for (let index = 0; index < buffer.length - 1; index += 1) { if (buffer[index] !== expected.slaveAddress) continue if (expectedFunctionCodes.indexOf(buffer[index + 1]) < 0) continue matchIndex = index break } if (matchIndex > 0) { buffer.splice(0, matchIndex) } else if (matchIndex < 0 && buffer.length > 2) { buffer.splice(0, buffer.length - 1) } } function readResponseFromBuffer(buffer, expected, options = {}) { if (!Array.isArray(buffer) || !buffer.length || !expected) { return { status: 'pending' } } alignResponseBuffer(buffer, expected) while (buffer.length >= 2) { const responseFunctionCode = buffer[1] const responseLength = getExpectedResponseLength(expected, responseFunctionCode, buffer) if (!responseLength) { return { status: 'pending' } } const frameLimit = normalizeMaxFrameBytes( options.maxFrameBytes === undefined ? expected.maxFrameBytes : options.maxFrameBytes ) if (frameLimit > 0 && responseLength > frameLimit) { return { frameLimit, responseLength, status: 'frame-too-long' } } if (buffer.length < responseLength) { return { status: 'pending' } } const frameBytes = buffer.slice(0, responseLength) const response = parseModbusResponse(frameBytes) if (!response) { return { frameBytes, status: 'invalid' } } const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode if (response.slaveAddress !== expected.slaveAddress || responseCode !== expected.functionCode) { buffer.shift() alignResponseBuffer(buffer, expected) continue } if (response.isException) { return { message: formatExceptionMessage(response), response, status: 'exception' } } if (!isExpectedResponse(response, expected)) { return { response, status: 'mismatch' } } buffer.splice(0, responseLength) return { response, status: 'complete' } } return { status: 'pending' } } function getSharedSlaveAddress(title = '从机地址错误') { try { return settingsService.getModbusSlaveAddress() } catch (error) { transport.showCommandAlert(title, error.message) return null } } function formatAddress(value) { return Number(value || 0).toString(16).toUpperCase() } function getChunkLabel(label, chunks, chunk) { if (!label || chunks.length <= 1) return label return `${label} ${formatAddress(chunk.address)}-${formatAddress(chunk.address + chunk.quantity - 1)}` } function isBroadcastAddress(slaveAddress) { return Number(slaveAddress) === 0 } function showBroadcastReadAlert(label) { transport.showCommandAlert(label || '读取命令错误', '广播地址 0x00 不支持读取') } function splitQuantity(startAddress, quantity, maxQuantity) { const chunks = [] let address = Number(startAddress) || 0 let remaining = Math.max(0, Math.floor(Number(quantity) || 0)) const chunkLimit = Math.max(1, Math.floor(Number(maxQuantity) || remaining || 1)) while (remaining > 0) { const chunkQuantity = Math.min(remaining, chunkLimit) chunks.push({ address, quantity: chunkQuantity }) address += chunkQuantity remaining -= chunkQuantity } return chunks } function getReadChunks(functionCode, startAddress, quantity, options = {}) { const maxQuantity = getMaxReadQuantity(functionCode, options.maxFrameBytes) return splitQuantity(startAddress, quantity, maxQuantity || quantity) } async function sendReadChunk(slaveAddress, functionCode, chunk, label, kind, options = {}) { if (isBroadcastAddress(slaveAddress)) { showBroadcastReadAlert(label) return false } return transport.sendManagedFrame( buildReadFrame(slaveAddress, functionCode, chunk.address, chunk.quantity, { maxFrameBytes: options.maxFrameBytes }), label, { address: chunk.address, functionCode, kind, protocol: PROTOCOL_NAME, quantity: chunk.quantity, slaveAddress }, { maxFrameBytes: options.maxFrameBytes, showModal: options.showModal } ) } async function readSpans(slaveAddress, functionCode, spans, label, kind, options = {}) { const readValues = { coils: {}, words: {} } const normalizedSpans = (spans || []).filter((span) => span && span.quantity > 0) for (const span of normalizedSpans) { const chunks = getReadChunks(functionCode, span.address, span.quantity, options) for (const chunk of chunks) { const responseValue = await sendReadChunk( slaveAddress, functionCode, chunk, getChunkLabel(label, chunks, chunk), kind, options ) if (!responseValue) return null if (functionCode === 0x01 || functionCode === 0x02) { addCoilReadValues(readValues, chunk.address, chunk.quantity, responseValue) } else { addWordReadValues(readValues, chunk.address, responseValue) } if (typeof options.onChunk === 'function') { options.onChunk(responseValue, chunk) } } } return readValues } async function readRegisterWords(slaveAddress, functionCode, startAddress, quantity, label, kind, options = {}) { const words = [] const chunks = getReadChunks(functionCode, startAddress, quantity, options) for (const chunk of chunks) { const responseValue = await sendReadChunk( slaveAddress, functionCode, chunk, getChunkLabel(label, chunks, chunk), kind, options ) if (!responseValue) return null const chunkWords = responseValue.words || [] chunkWords.forEach((word, index) => { words[chunk.address - startAddress + index] = Number(word) & 0xFFFF }) if (typeof options.onChunk === 'function') { options.onChunk(responseValue, chunk) } } return words } async function readBitValues(slaveAddress, functionCode, startAddress, quantity, label, kind, options = {}) { const result = await readSpans( slaveAddress, functionCode, [{ address: startAddress, quantity }], label, kind, options ) return result ? result.coils : null } async function readSingleHoldingWord(slaveAddress, address, label = '读取配对寄存器', kind = 'holding-word-read') { const words = await readRegisterWords(slaveAddress, 0x03, address, 1, label, kind) return words && Number.isInteger(words[0]) ? words[0] & 0xFFFF : null } function writeSingleCoil(slaveAddress, address, checked, label, kind = 'coil-write', options = {}) { const coilValue = checked ? 0xFF00 : 0x0000 return transport.sendManagedFrame( buildWriteSingleCoilFrame(slaveAddress, address, checked), label, isBroadcastAddress(slaveAddress) ? null : { address, functionCode: 0x05, kind, protocol: PROTOCOL_NAME, quantity: 1, slaveAddress, value: coilValue }, { maxFrameBytes: options.maxFrameBytes, showModal: options.showModal } ) } function writeSingleRegister(slaveAddress, address, value, label, kind = 'register-write', options = {}) { return transport.sendManagedFrame( buildWriteSingleRegisterFrame(slaveAddress, address, value), label, isBroadcastAddress(slaveAddress) ? null : { address, functionCode: 0x06, kind, protocol: PROTOCOL_NAME, quantity: 1, slaveAddress, value }, { maxFrameBytes: options.maxFrameBytes, showModal: options.showModal } ) } function writeMultipleRegisters(slaveAddress, address, values, label, kind = 'registers-write', options = {}) { return transport.sendManagedFrame( buildWriteMultipleRegistersFrame(slaveAddress, address, values, { maxFrameBytes: options.maxFrameBytes }), label, isBroadcastAddress(slaveAddress) ? null : { address, functionCode: 0x10, kind, protocol: PROTOCOL_NAME, quantity: values.length, slaveAddress }, { maxFrameBytes: options.maxFrameBytes, showModal: options.showModal } ) } const response = { MODBUS_EXCEPTION_MESSAGES, formatExceptionMessage, getExceptionText, getExpectedResponseLength, getReadBufferHint, isExpectedResponse, parseModbusRequest, parseModbusResponse, readResponseFromBuffer } const client = { getReadChunks, getSharedSlaveAddress, getMaxReadQuantity, readBitValues, readRegisterWords, readSingleHoldingWord, readSpans, splitQuantity, getMaxWriteMultipleRegisterQuantity, writeMultipleRegisters, writeSingleCoil, writeSingleRegister } module.exports = { MAX_MODBUS_DMA_BYTES, MAX_READ_COIL_QUANTITY, MAX_READ_REGISTER_QUANTITY, MAX_WRITE_MULTIPLE_REGISTER_QUANTITY, MODBUS_CRC_OPTIONS, MODBUS_EXCEPTION_MESSAGES, PROTOCOL_NAME, UNLIMITED_FRAME_BYTES, buildReadFrame, buildWriteMultipleRegistersFrame, buildWriteSingleCoilFrame, buildWriteSingleRegisterFrame, client, formatHex, getMaxReadQuantity, getMaxWriteMultipleRegisterQuantity, getReadResponseByteLength, hasValidCrc16Modbus, response }