const { buildReadFrame, buildWriteMultipleRegistersFrame, buildWriteSingleCoilFrame, buildWriteSingleRegisterFrame, formatHex } = require('../../protocols/modbus-rtu/frame.js') const transport = require('../../transport/ble-core.js') const { DATA_TYPE_OPTIONS, getDataType, getRegisterEncodedWords, isByteRegister, isTextRegister, normalizeRegister, validateRegisterValue } = require('../../domain/generic-modbus/model.js') const MODBUS_COMMANDS = [ { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' }, { key: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, inputMode: 'quantity' }, { key: 'readHolding', label: '03 读取保持寄存器', functionCode: 0x03, inputMode: 'quantity' }, { key: 'readInput', label: '04 读取输入寄存器', functionCode: 0x04, inputMode: 'quantity' }, { key: 'writeCoil', label: '05 写单线圈', functionCode: 0x05, inputMode: 'coil' }, { key: 'writeRegister', label: '06 写单寄存器', functionCode: 0x06, inputMode: 'single' }, { key: 'writeRegisters', label: '10 写多寄存器', functionCode: 0x10, inputMode: 'multiple' } ] const state = { commandIndex: 2, commandRegisterQuantity: '0001', commandValue: '0001', commandValueLabel: '读取数量', coilEnabled: true, generatedHex: '', protocolCommands: MODBUS_COMMANDS, protocolDataTypeOptions: DATA_TYPE_OPTIONS, protocolErrorText: '', protocolMultipleDialog: { visible: false }, protocolMultipleValues: [], registerAddress: '0000', showCoilValue: false, showCommandValue: true, showRegisterQuantity: false, slaveAddress: 'F0' } const subscribers = [] function setState(changedData) { Object.assign(state, changedData) subscribers.slice().forEach((subscriber) => { subscriber(getState()) }) } function getState() { return { ...state, protocolCommands: state.protocolCommands.slice(), protocolDataTypeOptions: state.protocolDataTypeOptions.slice(), protocolMultipleDialog: { ...state.protocolMultipleDialog }, protocolMultipleValues: state.protocolMultipleValues.map((item) => ({ ...item })) } } function subscribe(subscriber) { if (typeof subscriber !== 'function') return () => {} subscribers.push(subscriber) subscriber(getState()) return () => { const index = subscribers.indexOf(subscriber) if (index >= 0) subscribers.splice(index, 1) } } function parseHexNumber(value, label, maxValue) { const text = String(value || '').trim().replace(/^0x/i, '') if (!text || !/^[0-9a-fA-F]+$/.test(text)) { throw new Error(`${label}请输入十六进制数值`) } const parsedValue = parseInt(text, 16) if (parsedValue > maxValue) { throw new Error(`${label}超出范围`) } return parsedValue } function parseRegisterValues(value) { const text = String(value || '').trim() if (!text) throw new Error('请输入寄存器写入值') return text.split(/[\s,;]+/) .filter(Boolean) .map((item) => parseHexNumber(item, '写入值', 0xFFFF)) } function getCommand(index) { return MODBUS_COMMANDS[index] || MODBUS_COMMANDS[0] } function getDefaultCommandValue(command) { if (command.inputMode === 'quantity') return '0001' if (command.inputMode === 'coil') return 'ON' if (command.inputMode === 'multiple') return '0000' return '0000' } function normalizeManualMultipleQuantity(value) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text) return 1 if (/^[0-9a-fA-F]+$/.test(text)) return Math.max(1, Math.min(parseInt(text, 16), 0x007B)) const numberValue = Number(text) return Number.isFinite(numberValue) ? Math.max(1, Math.min(Math.round(numberValue), 0x007B)) : 1 } function formatManualMultipleQuantity(quantity) { return Number(quantity || 1).toString(16).toUpperCase().padStart(4, '0') } function createManualMultipleRegister(index, value = {}) { const dataType = getDataType(value.dataType || 'hex') const register = normalizeRegister({ dataType: dataType.key, inputValue: value.inputValue === undefined ? '' : value.inputValue, name: `寄存器 ${index + 1}`, textByteLength: value.textByteLength || (isTextRegister(dataType.key) ? '32' : '') }, { registerType: 'holding' }, index, Number(value.address || 0), 0) return { ...register, dataTypeIndex: DATA_TYPE_OPTIONS.findIndex((item) => item.key === register.dataType), inputValue: value.inputValue === undefined ? '' : value.inputValue } } function getManualRegisterWordCount(register) { return Math.max(1, Number(register && register.registerCount) || 1) } function normalizeManualMultipleValues(wordQuantity, values = [], startAddress = 0) { const result = [] let address = Number(startAddress) || 0 const endAddress = address + Math.max(1, Number(wordQuantity) || 1) let sourceIndex = 0 while (address < endAddress) { const current = values[sourceIndex] || {} let register = createManualMultipleRegister(result.length, { ...current, address }) const remainingWords = endAddress - address if (getManualRegisterWordCount(register) > remainingWords) { register = createManualMultipleRegister(result.length, { ...current, address, dataType: 'hex', inputValue: '' }) } result.push(register) address += getManualRegisterWordCount(register) sourceIndex += 1 } return result } function getManualMultipleWords(values = []) { const words = [] values.forEach((register) => { if (isByteRegister(register.dataType)) { const registerWords = getRegisterEncodedWords(register) if (!Array.isArray(registerWords) || !registerWords.length) throw new Error(`${register.name} 输入值无效`) words.push(Number(registerWords[0]) & 0x00FF) return } const registerWords = getRegisterEncodedWords(register) if (!Array.isArray(registerWords) || !registerWords.length) throw new Error(`${register.name} 输入值无效`) registerWords.forEach((word) => words.push(Number(word) & 0xFFFF)) }) return words } function getManualMultipleValueText(values = []) { try { return getManualMultipleWords(values).map((word) => word.toString(16).toUpperCase().padStart(4, '0')).join(' ') } catch (error) { return '' } } function generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity) { const slave = parseHexNumber(slaveAddress, '从站地址', 0xFF) const address = parseHexNumber(registerAddress, '起始地址', 0xFFFF) if (command.inputMode === 'quantity') { const quantity = parseHexNumber(commandValue, '读取数量', 0xFFFF) return buildReadFrame(slave, command.functionCode, address, quantity) } if (command.inputMode === 'coil') { return buildWriteSingleCoilFrame(slave, address, coilEnabled) } if (command.inputMode === 'single') { return buildWriteSingleRegisterFrame(slave, address, parseHexNumber(commandValue, '写入值', 0xFFFF)) } const words = parseRegisterValues(commandValue) const quantity = parseHexNumber(commandRegisterQuantity, '寄存器个数', 0xFFFF) if (quantity !== words.length) { throw new Error(`写入值数量应为 ${quantity} 个寄存器`) } return buildWriteMultipleRegistersFrame(slave, address, words) } function createProtocolState(commandIndex, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity) { const command = getCommand(commandIndex) const commandValueLabel = command.inputMode === 'quantity' ? '读取数量' : '写入值' try { return { commandValueLabel, generatedHex: formatHex(generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity)), protocolErrorText: '', showCoilValue: command.inputMode === 'coil', showRegisterQuantity: command.inputMode === 'multiple', showCommandValue: command.inputMode !== 'coil' } } catch (error) { return { commandValueLabel, generatedHex: '', protocolErrorText: error.message, showCoilValue: command.inputMode === 'coil', showRegisterQuantity: command.inputMode === 'multiple', showCommandValue: command.inputMode !== 'coil' } } } function setProtocolInput(changedData) { const command = getCommand(changedData.commandIndex === undefined ? state.commandIndex : changedData.commandIndex) let nextMultipleValues = changedData.protocolMultipleValues let nextCommandValue = changedData.commandValue if ( command.inputMode === 'multiple' && Object.prototype.hasOwnProperty.call(changedData, 'registerAddress') && !Object.prototype.hasOwnProperty.call(changedData, 'protocolMultipleValues') ) { try { const startAddress = parseHexNumber(changedData.registerAddress, '起始地址', 0xFFFF) nextMultipleValues = normalizeManualMultipleValues( normalizeManualMultipleQuantity(state.commandRegisterQuantity), state.protocolMultipleValues, startAddress ) nextCommandValue = getManualMultipleValueText(nextMultipleValues) } catch (error) {} } const nextState = { commandIndex: state.commandIndex, commandRegisterQuantity: state.commandRegisterQuantity, slaveAddress: state.slaveAddress, registerAddress: state.registerAddress, commandValue: state.commandValue, coilEnabled: state.coilEnabled, ...changedData, ...(nextMultipleValues ? { protocolMultipleValues: nextMultipleValues } : {}), ...(nextCommandValue !== undefined ? { commandValue: nextCommandValue } : {}) } setState({ ...changedData, ...(nextMultipleValues ? { protocolMultipleValues: nextMultipleValues } : {}), ...(nextCommandValue !== undefined ? { commandValue: nextCommandValue } : {}), ...createProtocolState( nextState.commandIndex, nextState.slaveAddress, nextState.registerAddress, nextState.commandValue, nextState.coilEnabled, nextState.commandRegisterQuantity ) }) } function setCommandIndex(index) { const commandIndex = Number(index) const command = getCommand(commandIndex) const commandValue = command.inputMode === 'multiple' ? getManualMultipleValueText(state.protocolMultipleValues) : getDefaultCommandValue(command) setProtocolInput({ commandIndex, commandValue, coilEnabled: true, commandRegisterQuantity: state.commandRegisterQuantity }) } function openProtocolMultipleDialog() { let startAddress = 0 try { startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) } catch (error) { setState({ protocolErrorText: error.message || '起始地址无效' }) return } const quantity = normalizeManualMultipleQuantity(state.commandRegisterQuantity) const values = normalizeManualMultipleValues(quantity, state.protocolMultipleValues, startAddress) setState({ commandRegisterQuantity: formatManualMultipleQuantity(quantity), protocolMultipleDialog: { title: '写多个寄存器', visible: true }, protocolMultipleValues: values }) } function closeProtocolMultipleDialog() { setState({ protocolMultipleDialog: { visible: false } }) } function setProtocolMultipleQuantity(value) { const quantity = normalizeManualMultipleQuantity(value) let startAddress = 0 try { startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) } catch (error) { setState({ commandRegisterQuantity: value, protocolErrorText: error.message || '起始地址无效' }) return } const values = normalizeManualMultipleValues(quantity, state.protocolMultipleValues, startAddress) const commandValue = getManualMultipleValueText(values) setProtocolInput({ commandRegisterQuantity: value, commandValue, protocolMultipleValues: values }) } function setProtocolMultipleValue(index, value) { const registerIndex = Number(index) const values = state.protocolMultipleValues.map((register, currentIndex) => ( currentIndex === registerIndex ? { ...register, inputValue: value } : register )) let startAddress = 0 try { startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) } catch (error) { setState({ protocolErrorText: error.message || '起始地址无效' }) return } const normalizedValues = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), values, startAddress) const commandValue = getManualMultipleValueText(normalizedValues) setProtocolInput({ commandValue, protocolMultipleValues: normalizedValues }) } function setProtocolMultipleType(index, dataTypeIndex) { const registerIndex = Number(index) const dataType = DATA_TYPE_OPTIONS[Number(dataTypeIndex)] || DATA_TYPE_OPTIONS[0] const changedValues = state.protocolMultipleValues.map((register, currentIndex) => ( currentIndex === registerIndex ? createManualMultipleRegister(currentIndex, { ...register, dataType: dataType.key, inputValue: '', textByteLength: isTextRegister(dataType.key) ? (register.textByteLength || '32') : '' }) : register )) let startAddress = 0 try { startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) } catch (error) { setState({ protocolErrorText: error.message || '起始地址无效' }) return } const values = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), changedValues, startAddress) setProtocolInput({ commandValue: getManualMultipleValueText(values), protocolMultipleValues: values }) } function setProtocolMultipleTextLength(index, value) { const registerIndex = Number(index) const changedValues = state.protocolMultipleValues.map((register, currentIndex) => ( currentIndex === registerIndex ? createManualMultipleRegister(currentIndex, { ...register, textByteLength: value }) : register )) let startAddress = 0 try { startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) } catch (error) { setState({ protocolErrorText: error.message || '起始地址无效' }) return } const values = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), changedValues, startAddress) setProtocolInput({ commandValue: getManualMultipleValueText(values), protocolMultipleValues: values }) } function validateProtocolMultipleValue(index, value) { const register = state.protocolMultipleValues[Number(index)] if (!register) return false return validateRegisterValue(register, value) } function buildGeneratedExpectedResponse() { try { const command = getCommand(state.commandIndex) const address = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF) const slaveAddress = parseHexNumber(state.slaveAddress, '从站地址', 0xFF) const quantity = command.inputMode === 'quantity' ? parseHexNumber(state.commandValue, '读取数量', 0xFFFF) : (command.inputMode === 'multiple' ? parseHexNumber(state.commandRegisterQuantity, '寄存器个数', 0xFFFF) : 1) const value = command.inputMode === 'coil' ? (state.coilEnabled ? 0xFF00 : 0x0000) : (command.inputMode === 'single' ? parseHexNumber(state.commandValue, '写入值', 0xFFFF) : undefined) return { address, functionCode: command.functionCode, kind: 'manual-rtu', quantity, value, slaveAddress } } catch (error) { return null } } function sendGeneratedFrame() { if (!state.generatedHex) return false const expected = buildGeneratedExpectedResponse() return transport.enqueueSendFrame(state.generatedHex, 'RTU', expected ? { expected } : {}) } setState(createProtocolState( state.commandIndex, state.slaveAddress, state.registerAddress, state.commandValue, state.coilEnabled, state.commandRegisterQuantity )) module.exports = { buildGeneratedExpectedResponse, closeProtocolMultipleDialog, getState, openProtocolMultipleDialog, sendGeneratedFrame, setCommandIndex, setProtocolInput, setProtocolMultipleQuantity, setProtocolMultipleTextLength, setProtocolMultipleType, setProtocolMultipleValue, subscribe, validateProtocolMultipleValue }