const { formatExportStamp, isCancelError, loadSelectedFile, saveTextFileToChat } = require('../../repositories/file.js') const { getWxApi } = require('../../utils/platform-utils.js') const { bytesToWords } = require('../../utils/binary-utils.js') const { parseHexInteger } = require('../../utils/base-utils.js') const transport = require('../../transport/ble-core.js') const settingsService = require('../../store/settings-store.js') const modbusClient = require('../../protocols/modbus-rtu/client.js') const { DATA_TYPE_OPTIONS, REGISTER_TYPE_OPTIONS, cloneImportedGroup, decodeRegisterFromByteCache, decodeRegisterFromWordCache, decodeRegisterValue, formatCoilDisplayValue, formatRegisterValue, getDataType, getGroupEncodedBytes, getGroupEncodedWords, getRegisterEncodedBytes, getRegisterEncodedWords, getRegisterBytesFromByteCache, getRegisterJsonValue, getRegisterWordsFromByteCache, getRegisterWordsFromWordCache, getRegisterWriteValueText, isAddressRangeOverflow, isBitRegisterType, isByteRegister, normalizeGroup, normalizeGroupConfig, parseCoilValue, registerTypeIsBit, splitWordSpans, validateRegisterValue } = require('../../domain/generic-modbus/model.js') const { parseStructCatalog, parseStructDefinition: parseStructDefinitionSource } = require('../../domain/generic-modbus/struct-parser.js') const { createGroupsFromCodeInfo, parseModbusCodeInfo } = require('../../domain/generic-modbus/code-info-parser.js') const STORAGE_KEY = 'generic-modbus-groups-json' const JSON_DOCUMENT_TYPE = 'generic-modbus-rtu' const JSON_SCHEMA_VERSION = 2 const DEBUG_MEMORY_TYPES = { DATA: 0x00, BIT: 0x00, IDATA: 0x01, XDATA: 0x02, CODE: 0x03 } const GROUP_SOURCE_FIELDS = [ 'addressUnit', 'sourceAddress', 'sourceAddressText', 'sourceByteLength', 'sourceMemoryArea', 'sourceMemoryClass', 'sourceSegment', 'sourceSegmentModule', 'sourceSymbolName' ] const REGISTER_SOURCE_FIELDS = [ 'sourceAddress', 'sourceAddressText', 'sourceByteLength', 'sourceBitOffset', 'sourceBitWidth', 'sourceMemoryArea', 'sourceMemoryClass', 'sourceSymbolName', 'sourceSymbolType' ] const REGISTER_STRUCT_FIELDS = [ 'bitOffset', 'bitWidth', 'byteStart', 'isBitField', 'structByteLength' ] let initialized = false const subscribers = [] let state = { genericModbusDataTypeOptions: DATA_TYPE_OPTIONS, genericModbusGroups: [], genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS } function notify() { const nextState = getState() subscribers.slice().forEach((subscriber) => { subscriber(nextState) }) } function setState(changedData, options = {}) { state = { ...state, ...changedData } if (options.persist !== false) persistGroups() notify() } function resolveMaxPacketLength(value) { const settings = settingsService.getState() const numberValue = Number(value === undefined ? settings.genericModbusMaxPacketLength : value) if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0 if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue) return 64 } function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) { if (maxPacketLength === 0) return Math.max(1, totalQuantity) return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength)) } function pickFields(source, fields) { return fields.reduce((result, field) => { if (source && source[field] !== undefined && source[field] !== null && source[field] !== '') { result[field] = source[field] } return result }, {}) } function toPersistedGroups(groups) { return groups.map((group) => ({ layout: group.layout, name: group.name, registerType: group.registerType, startAddress: group.startAddress, quantity: group.quantity, registers: group.registers.map((register) => ({ dataType: register.dataType, defaultValue: register.defaultValue, isStructField: register.isStructField, name: register.name, maxValue: register.maxValue, minValue: register.minValue, textByteLength: register.textByteLength, remark: register.remark, unit: register.unit, value: getRegisterJsonValue(register), ...pickFields(register, REGISTER_STRUCT_FIELDS), ...pickFields(register, REGISTER_SOURCE_FIELDS) })), ...pickFields(group, GROUP_SOURCE_FIELDS) })) } function toJsonData(groups = state.genericModbusGroups, options = {}) { const jsonData = { groups: toPersistedGroups(groups), type: JSON_DOCUMENT_TYPE, version: JSON_SCHEMA_VERSION } if (options.includeExportedAt) { jsonData.exportedAt = new Date().toISOString() } return jsonData } function toJsonText(groups = state.genericModbusGroups, options = {}) { return JSON.stringify(toJsonData(groups, options), null, 2) } function parseJsonGroups(jsonText) { const parsed = typeof jsonText === 'string' ? JSON.parse(jsonText) : jsonText const groups = Array.isArray(parsed) ? parsed : (Array.isArray(parsed && parsed.groups) ? parsed.groups : parsed && parsed.genericModbusGroups) if (parsed && parsed.type && parsed.type !== JSON_DOCUMENT_TYPE) { throw new Error('JSON 文件不是通用Modbus配置') } if (parsed && parsed.version && parsed.version !== JSON_SCHEMA_VERSION) { throw new Error('JSON 版本不兼容') } if (!Array.isArray(groups)) { throw new Error('JSON 中没有找到寄存器组数组') } return groups } function normalizeDuplicateText(value) { return String(value === undefined || value === null ? '' : value) .trim() .toLowerCase() } function normalizeAddressKey(value, textValue) { const numberValue = Number(value) if (Number.isFinite(numberValue)) return String(Math.floor(numberValue)) return String(textValue === undefined || textValue === null ? '' : textValue) .trim() .toUpperCase() } function normalizeBitKey(source = {}) { const value = source.sourceBitOffset !== undefined && source.sourceBitOffset !== null && source.sourceBitOffset !== '' ? source.sourceBitOffset : source.bitOffset const numberValue = Number(value) return Number.isFinite(numberValue) ? String(Math.floor(numberValue)) : '' } function getRegisterDuplicateKey(register = {}, group = {}) { const area = normalizeDuplicateText(register.sourceMemoryArea || group.sourceMemoryArea || register.memoryArea || '') const symbolName = normalizeDuplicateText(register.sourceSymbolName || register.name || '') if (area && symbolName) return ['register', area, symbolName].join('|') const addressKey = normalizeAddressKey( register.sourceAddress !== undefined ? register.sourceAddress : register.address, register.sourceAddressText || register.addressText ) const bitKey = normalizeBitKey(register) if (!area && !symbolName && !addressKey) return '' return ['register', area, symbolName, addressKey, bitKey].join('|') } function isSingleRegisterAggregateGroup(group = {}) { const groupSymbolName = normalizeDuplicateText(group.sourceSymbolName || group.name || '') const registers = Array.isArray(group.registers) ? group.registers : [] return registers.some((register) => { const registerSymbolName = normalizeDuplicateText(register.sourceSymbolName || register.name || '') return registerSymbolName && groupSymbolName && registerSymbolName !== groupSymbolName }) } function getGroupDuplicateKey(group = {}) { const area = normalizeDuplicateText(group.sourceMemoryArea || '') const symbolName = normalizeDuplicateText(group.sourceSymbolName || group.name || '') if (area && symbolName) return ['group', area, symbolName].join('|') const addressKey = normalizeAddressKey( group.sourceAddress !== undefined ? group.sourceAddress : group.startAddress, group.sourceAddressText || group.startAddressText ) if (!area && !symbolName && !addressKey) return '' return ['group', area, symbolName, addressKey].join('|') } function getAggregateGroupDuplicateKey(source = {}) { const area = normalizeDuplicateText(source.sourceMemoryArea || source.memoryArea || '') const registerType = normalizeDuplicateText(source.registerType || '') const segment = normalizeDuplicateText(source.sourceSegment || '') return ['aggregate', area, registerType, segment].join('|') } function collectImportedVariableIndexes(groups = []) { return groups.reduce((indexes, group, groupIndex) => { if (!isSingleRegisterAggregateGroup(group)) { const groupKey = getGroupDuplicateKey(group) if (groupKey) indexes.groupIndexes[groupKey] = groupIndex } else { const aggregateKey = getAggregateGroupDuplicateKey(group) if (aggregateKey) indexes.aggregateGroupIndexes[aggregateKey] = groupIndex } ;(Array.isArray(group.registers) ? group.registers : []).forEach((register) => { const registerKey = getRegisterDuplicateKey(register, group) if (registerKey) { indexes.registerIndexes[registerKey] = { groupIndex, registerIndex: group.registers.indexOf(register) } } }) return indexes }, { aggregateGroupIndexes: {}, groupIndexes: {}, registerIndexes: {} }) } function mergeImportedGroupState(existingGroup, incomingGroup) { if (!existingGroup) return incomingGroup return { ...incomingGroup, deleteVisible: false, expanded: existingGroup.expanded === true, id: existingGroup.id } } function mergeImportedRegisterState(existingRegister, incomingRegister) { if (!existingRegister) return incomingRegister return { ...incomingRegister, id: existingRegister.id, inputValue: incomingRegister.inputValue !== undefined && incomingRegister.inputValue !== null ? incomingRegister.inputValue : existingRegister.inputValue, rawBytes: [], rawValue: null, rawWords: [] } } function mergeAggregateImportedGroup(nextGroups, incomingGroup, indexes) { const aggregateKey = getAggregateGroupDuplicateKey(incomingGroup) const aggregateGroupIndex = indexes.aggregateGroupIndexes[aggregateKey] let targetGroup = aggregateGroupIndex === undefined ? null : nextGroups[aggregateGroupIndex] let targetGroupIndex = aggregateGroupIndex let targetRegisters = targetGroup && Array.isArray(targetGroup.registers) ? targetGroup.registers.slice() : [] let addedRegisterCount = 0 let updatedRegisterCount = 0 ;(Array.isArray(incomingGroup.registers) ? incomingGroup.registers : []).forEach((incomingRegister) => { const registerKey = getRegisterDuplicateKey(incomingRegister, incomingGroup) const existingRef = registerKey ? indexes.registerIndexes[registerKey] : null if (existingRef) { const existingGroup = nextGroups[existingRef.groupIndex] const existingRegister = existingGroup && existingGroup.registers ? existingGroup.registers[existingRef.registerIndex] : null const mergedRegister = mergeImportedRegisterState(existingRegister, incomingRegister) if (targetGroupIndex !== undefined && existingRef.groupIndex === targetGroupIndex) { targetRegisters[existingRef.registerIndex] = mergedRegister } else if (existingGroup) { const registers = existingGroup.registers.slice() registers[existingRef.registerIndex] = mergedRegister nextGroups[existingRef.groupIndex] = normalizeGroup({ ...existingGroup, registers }) } updatedRegisterCount += 1 return } targetRegisters.push(incomingRegister) addedRegisterCount += 1 }) if (targetGroupIndex !== undefined && targetGroup) { nextGroups[targetGroupIndex] = normalizeGroup(mergeImportedGroupState(targetGroup, { ...incomingGroup, quantity: targetRegisters.length, registers: targetRegisters })) } else if (targetRegisters.length) { targetGroup = normalizeGroup({ ...incomingGroup, quantity: targetRegisters.length, registers: targetRegisters }) nextGroups.push(targetGroup) targetGroupIndex = nextGroups.length - 1 } return { addedGroupCount: targetGroupIndex === aggregateGroupIndex ? 0 : (targetRegisters.length ? 1 : 0), addedRegisterCount, updatedGroupCount: targetGroupIndex === aggregateGroupIndex && (addedRegisterCount || updatedRegisterCount) ? 1 : 0, updatedRegisterCount } } function mergeImportedGroups(existingGroups = [], incomingGroups = []) { const nextGroups = existingGroups.slice() let indexes = collectImportedVariableIndexes(nextGroups) const result = { addedGroupCount: 0, addedRegisterCount: 0, groups: nextGroups, updatedGroupCount: 0, updatedRegisterCount: 0 } incomingGroups.forEach((incomingGroup) => { if (isSingleRegisterAggregateGroup(incomingGroup)) { const aggregateResult = mergeAggregateImportedGroup(nextGroups, incomingGroup, indexes) result.addedGroupCount += aggregateResult.addedGroupCount result.addedRegisterCount += aggregateResult.addedRegisterCount result.updatedGroupCount += aggregateResult.updatedGroupCount result.updatedRegisterCount += aggregateResult.updatedRegisterCount indexes = collectImportedVariableIndexes(nextGroups) return } const groupKey = getGroupDuplicateKey(incomingGroup) const existingGroupIndex = groupKey ? indexes.groupIndexes[groupKey] : undefined if (existingGroupIndex !== undefined) { const existingGroup = nextGroups[existingGroupIndex] nextGroups[existingGroupIndex] = normalizeGroup(mergeImportedGroupState(existingGroup, incomingGroup)) result.updatedGroupCount += 1 } else { nextGroups.push(incomingGroup) result.addedGroupCount += 1 } indexes = collectImportedVariableIndexes(nextGroups) }) result.changedCount = result.addedGroupCount + result.updatedGroupCount + result.addedRegisterCount + result.updatedRegisterCount return result } function readStoredGroups() { const wxApi = getWxApi() if (typeof wxApi.getStorageSync !== 'function') return [] try { const jsonText = wxApi.getStorageSync(STORAGE_KEY) if (jsonText) return parseJsonGroups(jsonText).map(cloneImportedGroup) } catch (error) { return [] } return [] } function persistGroups() { const wxApi = getWxApi() if (typeof wxApi.setStorageSync !== 'function') return try { wxApi.setStorageSync(STORAGE_KEY, toJsonText()) } catch (error) {} } function init() { if (initialized) return state = { ...state, genericModbusGroups: readStoredGroups().map(normalizeGroup) } initialized = true } function getState() { return { ...state, genericModbusDataTypeOptions: DATA_TYPE_OPTIONS, genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS } } function subscribe(subscriber) { if (typeof subscriber !== 'function') return () => {} init() subscribers.push(subscriber) subscriber(getState()) return () => { const index = subscribers.indexOf(subscriber) if (index >= 0) subscribers.splice(index, 1) } } function getShareFileName() { return `generic-modbus-rtu-${formatExportStamp()}.json` } async function importJsonFromMessageFile() { try { const file = await loadSelectedFile('message', { encoding: 'utf8', extensionMessage: '请选择 .json 寄存器配置文件', extensions: ['json'], fallbackName: 'generic-modbus.json' }) const jsonText = file.text const importedGroups = parseJsonGroups(jsonText).map(cloneImportedGroup).map(normalizeGroup) if (!importedGroups.length) throw new Error('JSON 中没有可导入的寄存器组') const merged = mergeImportedGroups(state.genericModbusGroups, importedGroups) setState({ genericModbusGroups: merged.groups }) return merged.changedCount || 0 } catch (error) { const message = error && error.message ? error.message : '导入通用Modbus配置失败' transport.showCommandAlert('通用Modbus导入', message) return 0 } } function getRegisterByteLengthFromConfig(register) { const dataType = getDataType(register.dataType).key if (dataType === 'ascii' || dataType === 'utf8') return Math.max(1, Number(register.textByteLength) || 1) if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4 if (dataType === 'int8_t' || dataType === 'uint8_t') return 1 return 2 } function getRegistersByteLength(registers = []) { const explicitByteEnds = registers.map((register) => { const byteStart = Number(register && register.byteStart) if (!Number.isFinite(byteStart)) return null if (register.isBitField) { const bitOffset = Math.min(Math.max(Math.floor(Number(register.bitOffset) || 0), 0), 7) const bitWidth = Math.max(1, Math.round(Number(register.bitWidth) || 1)) return Math.max(0, Math.floor(byteStart)) + Math.max(1, Math.ceil((bitOffset + bitWidth) / 8)) } return Math.max(0, Math.floor(byteStart)) + getRegisterByteLengthFromConfig(register) }).filter((value) => Number.isFinite(value)) if (explicitByteEnds.length) return Math.max.apply(null, explicitByteEnds) return registers.reduce((total, register) => total + getRegisterByteLengthFromConfig(register), 0) } function normalizeSymbolText(value) { return String(value || '') .replace(/^_+/, '') .replace(/[^A-Za-z0-9]/g, '') .toLowerCase() } function findStructCompletion(group, catalog) { const symbolName = group.sourceSymbolName || group.name const direct = catalog.variablesByName[normalizeSymbolText(symbolName)] || catalog.variablesByName[symbolName] if (direct) return direct const normalizedSymbol = normalizeSymbolText(symbolName) const normalizedType = normalizeSymbolText(group.sourceSymbolType) const expectedBytes = Number(group.sourceByteLength || group.byteLength || 0) const matchedStruct = catalog.structs.find((structInfo) => { const normalizedStructName = normalizeSymbolText(structInfo.name) if (normalizedType && normalizedType === normalizedStructName) { return true } if (normalizedSymbol && ( normalizedSymbol === normalizedStructName || normalizedSymbol.indexOf(normalizedStructName) >= 0 || normalizedStructName.indexOf(normalizedSymbol) >= 0 )) { return true } return expectedBytes > 0 && getRegistersByteLength(structInfo.registers) === expectedBytes }) return matchedStruct ? { name: symbolName, registers: matchedStruct.registers, structName: matchedStruct.name } : null } function createCompletedRegisters(group, completion) { return completion.registers.map((register) => ({ ...register, isStructField: true, remark: [ register.remark, `${group.sourceMemoryArea || ''} ${group.sourceAddressText || group.startAddressText || ''}`.trim(), completion.structName ? `struct ${completion.structName}` : '', register.isBitField ? `bit${register.bitOffset}:${register.bitWidth}` : '' ].filter(Boolean).join(' · '), sourceMemoryArea: group.sourceMemoryArea, sourceMemoryClass: group.sourceMemoryClass, sourceSymbolName: group.sourceSymbolName, sourceSymbolType: completion.structName || register.sourceSymbolType })) } function completeStructInstanceGroups(groups, sourceText, options = {}) { const catalog = parseStructCatalog(sourceText) let completedCount = 0 let skippedCount = 0 const nextGroups = groups.map((group) => { if (!group.sourceSymbolName || !group.sourceMemoryArea) return group const completion = findStructCompletion(group, catalog) if (!completion || !completion.registers || !completion.registers.length) { skippedCount += 1 return group } const expectedBytes = Number(group.sourceByteLength || group.byteLength || 0) const actualBytes = getRegistersByteLength(completion.registers) if (expectedBytes > 0 && actualBytes !== expectedBytes && options.strictLength !== false) { skippedCount += 1 return group } completedCount += 1 return normalizeGroup({ ...group, layout: 'struct', quantity: completion.registers.length, registers: createCompletedRegisters(group, completion) }) }) return { completedCount, groups: nextGroups, skippedCount, structCount: catalog.structs.length, variableCount: Object.keys(catalog.variablesByName).length } } function completeStructInstanceGroupsWithStructSource(sourceText, options = {}) { const completed = completeStructInstanceGroups(state.genericModbusGroups, sourceText, options) if (!completed.completedCount) { throw new Error('没有找到可匹配的结构体实例') } setState({ genericModbusGroups: completed.groups }) return completed } async function completeStructInstanceGroupsWithStructFile(options = {}) { try { const file = await loadSelectedFile('message', { encoding: 'utf8', extensionMessage: '请选择 .h 或 .c 结构体定义文件', extensions: ['h', 'c', 'txt'], fallbackName: 'structs.h' }) return completeStructInstanceGroupsWithStructSource(file.text, options) } catch (error) { const message = error && error.message ? error.message : '结构体补全失败' transport.showCommandAlert('结构体补全', message) return { completedCount: 0, skippedCount: 0, structCount: 0, variableCount: 0 } } } async function queryCodeInfoBlock(options = {}) { const slaveAddress = modbusClient.getSharedSlaveAddress() if (slaveAddress === null) { return { ok: false } } const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength) const result = await modbusClient.readCodeInfoBlock( slaveAddress, '查询Code信息块', 'generic-modbus-code-info-read', { maxFrameBytes: maxPacketLength, showModal: options.showModal !== false } ) if (!result) { return { ok: false } } const codeInfo = parseModbusCodeInfo(result.bytes) const importedGroups = createGroupsFromCodeInfo(codeInfo, options).map(cloneImportedGroup).map(normalizeGroup) const merged = mergeImportedGroups(state.genericModbusGroups, importedGroups) if (importedGroups.length) { setState({ genericModbusGroups: merged.groups }) } return { address: result.address, addedGroups: merged.addedGroupCount, addedRegisters: merged.addedRegisterCount, byteLength: result.byteLength, bytes: result.bytes, codeInfo, groupCount: importedGroups.length, memoryType: result.memoryType, ok: true, structCount: codeInfo.structCount, updatedGroups: merged.updatedGroupCount, updatedRegisters: merged.updatedRegisterCount } } async function saveJsonToChat() { try { if (!state.genericModbusGroups.length) { throw new Error('没有可保存的寄存器组') } const jsonText = toJsonText(state.genericModbusGroups, { includeExportedAt: true }) await saveTextFileToChat(getShareFileName(), jsonText) return state.genericModbusGroups.length } catch (error) { const message = error && error.message ? error.message : '保存通用Modbus配置失败' if (!isCancelError(error)) { transport.showCommandAlert('通用Modbus保存', message) } return 0 } } function addGroupFromConfig(config = {}) { let groupConfig try { groupConfig = normalizeGroupConfig(config) } catch (error) { transport.showCommandAlert('通用Modbus添加', error.message || '寄存器组配置无效') return null } if (isAddressRangeOverflow(groupConfig.startAddress, groupConfig.quantity)) { transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF') return null } const registers = Array.isArray(config.registers) ? config.registers : [] const group = normalizeGroup({ ...groupConfig, layout: config.layout, ...(registers.length ? { registers } : {}), expanded: false }) if (group.addressOverflow) { transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF') return null } setState({ genericModbusGroups: state.genericModbusGroups.concat(group) }) return group } function updateGroupConfig(groupId, config = {}) { const group = findGroup(groupId) if (!group) return null let nextConfig try { nextConfig = normalizeGroupConfig({ ...group, ...config }) } catch (error) { transport.showCommandAlert('通用Modbus更新', error.message || '寄存器组配置无效') return null } if (isAddressRangeOverflow(nextConfig.startAddress, nextConfig.quantity)) { transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF') return null } const registers = Array.isArray(config.registers) ? config.registers : group.registers const updatedGroup = normalizeGroup({ ...group, ...nextConfig, registers }) if (updatedGroup.addressOverflow) { transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF') return null } setState({ genericModbusGroups: state.genericModbusGroups.map((item) => ( item.id === groupId ? updatedGroup : item )) }) return updatedGroup } function updateGroups(mapper) { setState({ genericModbusGroups: state.genericModbusGroups.map((group, index) => normalizeGroup(mapper(group, index))) }) } function findGroup(groupId) { return state.genericModbusGroups.find((group) => group.id === groupId) } function setGroupExpanded(groupId, expanded) { updateGroups((group) => group.id === groupId ? { ...group, deleteVisible: false, expanded } : group) } function setGroupDeleteVisible(groupId, deleteVisible) { updateGroups((group) => group.id === groupId ? { ...group, deleteVisible } : group) } function removeGroup(groupId) { setState({ genericModbusGroups: state.genericModbusGroups.filter((group) => group.id !== groupId) }) } function reorderRegister(groupId, fromIndex, toIndex) { const group = findGroup(groupId) if (!group) return null if (group.isStructLayout) return group const registers = group.registers.slice() const sourceIndex = Number(fromIndex) const targetIndex = Number(toIndex) if (!Number.isInteger(sourceIndex) || !Number.isInteger(targetIndex)) return null if (sourceIndex < 0 || sourceIndex >= registers.length) return null const safeTargetIndex = Math.min(Math.max(targetIndex, 0), registers.length - 1) if (safeTargetIndex === sourceIndex) return group const moved = registers.splice(sourceIndex, 1)[0] registers.splice(safeTargetIndex, 0, moved) const updatedGroup = normalizeGroup({ ...group, quantity: registers.length, registers }) setState({ genericModbusGroups: state.genericModbusGroups.map((item) => ( item.id === groupId ? updatedGroup : item )) }) return updatedGroup } function updateRegister(groupId, registerIndex, changedData) { updateGroups((group) => { if (group.id !== groupId) return group const shouldResetReadState = Object.prototype.hasOwnProperty.call(changedData, 'dataType') || Object.prototype.hasOwnProperty.call(changedData, 'textByteLength') return { ...group, registers: group.registers.map((register, currentIndex) => ( currentIndex === registerIndex ? { ...register, ...(shouldResetReadState ? { rawValue: null, rawWords: [] } : {}), ...changedData } : register )) } }) } function updateRegisterValue(groupId, registerIndex, value) { updateRegister(groupId, registerIndex, { inputValue: value, isDirty: true }) } function validateRegisterInputValue(groupId, registerIndex, value) { const group = findGroup(groupId) if (!group) return false const register = group.registers[registerIndex] if (!register) return false return validateRegisterValue(register, value) } function parseStructDefinition(sourceText) { return parseStructDefinitionSource(sourceText) } function getDebugMemoryType(group = {}) { const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase() return Object.prototype.hasOwnProperty.call(DEBUG_MEMORY_TYPES, memoryArea) ? DEBUG_MEMORY_TYPES[memoryArea] : null } function isDebugMemoryGroup(group = {}) { return getDebugMemoryType(group) !== null } function isByteAddressedGroup(group = {}) { return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || isDebugMemoryGroup(group) } function getDebugMemoryAddress(group = {}) { const sourceAddress = Number(group.sourceAddress) if (Number.isFinite(sourceAddress)) return Math.max(0, Math.floor(sourceAddress)) & 0xFFFF return Math.max(0, Number(group.startAddress) || 0) & 0xFFFF } function getDebugMemoryByteLength(group = {}) { const sourceByteLength = Number(group.sourceByteLength) if (Number.isFinite(sourceByteLength) && sourceByteLength > 0) return Math.min(0xFFFF, Math.ceil(sourceByteLength)) const byteLength = Number(group.byteLength) if (Number.isFinite(byteLength) && byteLength > 0) return Math.min(0xFFFF, Math.ceil(byteLength)) return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2)) } function bytesToPaddedWords(bytes = []) { return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0)) } function fillByteCacheFromBytes(byteCache, startAddress, bytes = []) { bytes.forEach((byte, offset) => { byteCache[startAddress + offset] = Number(byte) & 0xFF }) } function fillWordCacheFromBytes(wordCache, startAddress, bytes = []) { const words = bytesToPaddedWords(bytes) words.forEach((word, offset) => { wordCache[startAddress + offset] = Number(word) & 0xFFFF }) } function createWrittenRegisterSnapshots(group, wordCache) { return group.registers.map((register) => { const rawBytes = isByteAddressedGroup(group) ? (getRegisterBytesFromByteCache(register, wordCache) || []) : [] const rawWords = isByteAddressedGroup(group) ? (getRegisterWordsFromByteCache(register, wordCache) || []) : (getRegisterWordsFromWordCache(register, wordCache) || []) const rawValue = isByteAddressedGroup(group) ? decodeRegisterFromByteCache(register, wordCache) : decodeRegisterValue(register, rawWords) return { rawBytes, rawWords, rawValue, displayValue: formatRegisterValue(register, rawValue) } }) } function createWrittenRegisterSnapshot(group, register, byteCache) { const snapshots = createWrittenRegisterSnapshots({ ...group, registers: [register] }, byteCache) return snapshots[0] || null } function groupHasBitFields(group = {}) { return (Array.isArray(group.registers) ? group.registers : []).some((register) => !!register.isBitField) } async function writeDebugMemoryRegister(group, register, slaveAddress) { const memoryType = getDebugMemoryType(group) const maxPacketLength = resolveMaxPacketLength() const byteLength = Math.max(1, Number(register.byteLength) || 1) const address = Math.max(0, Math.floor(Number(register.address) || getDebugMemoryAddress(group))) & 0xFFFF let bytes if (memoryType === null) { transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } if (memoryType === 0x03) { transport.showCommandAlert('内存写入', 'code 区暂不支持写入') return null } if (slaveAddress === 0x00 && register.isBitField) { transport.showCommandAlert('内存写入', '位域变量需要先读后写,不能使用广播地址') return null } try { if (register.isBitField) { const baseBytes = await modbusClient.readDebugMemory( slaveAddress, memoryType, address, byteLength, register.name ? `${register.name} 读改写` : '变量读改写', 'generic-modbus-debug-register-rmw-read', { maxFrameBytes: maxPacketLength } ) if (!baseBytes) return null bytes = getGroupEncodedBytes({ ...group, paddedByteLength: byteLength, registers: [{ ...register, address, byteStart: 0 }] }, baseBytes).slice(0, byteLength) } else { bytes = getRegisterEncodedBytes(register) } } catch (error) { transport.showCommandAlert('内存写入', error.message || '变量没有有效写入值') return null } if (!Array.isArray(bytes) || !bytes.length) { transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`) return null } bytes = bytes.slice(0, byteLength) while (bytes.length < byteLength) bytes.push(0) const ok = await modbusClient.writeDebugMemory( slaveAddress, memoryType, address, bytes, register.name || group.name || '变量写入', 'generic-modbus-debug-register-write', { maxFrameBytes: maxPacketLength } ) if (!ok) return null const byteCache = {} fillByteCacheFromBytes(byteCache, address, bytes) return createWrittenRegisterSnapshot(group, register, byteCache) } async function readDebugMemoryGroup(group, slaveAddress, options = {}) { const memoryType = getDebugMemoryType(group) const address = getDebugMemoryAddress(group) const byteLength = getDebugMemoryByteLength(group) const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength) if (memoryType === null) { transport.showCommandAlert('内存读取', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } const bytes = await modbusClient.readDebugMemory( slaveAddress, memoryType, address, byteLength, group.name || '内存读取', 'generic-modbus-debug-read', { maxFrameBytes: maxPacketLength, showModal: options.showModal !== false } ) if (!bytes) return null const wordCache = {} if (isByteAddressedGroup(group)) { fillByteCacheFromBytes(wordCache, group.startAddress, bytes) } else { fillWordCacheFromBytes(wordCache, group.startAddress, bytes) } return wordCache } async function writeDebugMemoryGroup(group, slaveAddress) { const memoryType = getDebugMemoryType(group) const address = getDebugMemoryAddress(group) const byteLength = getDebugMemoryByteLength(group) const maxPacketLength = resolveMaxPacketLength() let bytes if (memoryType === null) { transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } if (memoryType === 0x03) { transport.showCommandAlert('内存写入', 'code 区暂不支持写入') return null } if (slaveAddress === 0x00 && groupHasBitFields(group)) { transport.showCommandAlert('内存写入', '位域变量需要先读后写,不能使用广播地址') return null } try { let baseBytes = null if (groupHasBitFields(group)) { baseBytes = await modbusClient.readDebugMemory( slaveAddress, memoryType, address, byteLength, group.name ? `${group.name} 读改写` : '内存读改写', 'generic-modbus-debug-rmw-read', { maxFrameBytes: maxPacketLength } ) if (!baseBytes) return null } bytes = getGroupEncodedBytes(group, baseBytes) } catch (error) { transport.showCommandAlert('内存写入', error.message || '寄存器组没有有效写入值') return null } bytes = bytes.slice(0, byteLength) const ok = await modbusClient.writeDebugMemory( slaveAddress, memoryType, address, bytes, group.name || '内存写入', 'generic-modbus-debug-write', { maxFrameBytes: maxPacketLength } ) if (!ok) return null const wordCache = {} if (isByteAddressedGroup(group)) { fillByteCacheFromBytes(wordCache, group.startAddress, bytes) } else { fillWordCacheFromBytes(wordCache, group.startAddress, bytes) } return createWrittenRegisterSnapshots(group, wordCache) } async function readGroup(groupId, options = {}) { const group = findGroup(groupId) const slaveAddress = modbusClient.getSharedSlaveAddress() if (!group || slaveAddress === null) return false if (group.addressOverflow) { transport.showCommandAlert('通用Modbus读取', '寄存器地址范围超出 0xFFFF') return false } const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0) const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength) const wordCache = {} if (isDebugMemoryGroup(group)) { const debugWordCache = await readDebugMemoryGroup(group, slaveAddress, options) if (!debugWordCache) return false Object.keys(debugWordCache).forEach((addressText) => { const numericAddress = Number(addressText) if (Number.isFinite(numericAddress)) { wordCache[numericAddress] = Number(debugWordCache[addressText]) & 0xFFFF } }) } else { const values = await modbusClient.readSpans( slaveAddress, group.functionCode, [{ address: group.startAddress, quantity: totalQuantity }], group.name || '通用Modbus读取', 'generic-modbus-read', { maxFrameBytes: maxPacketLength, showModal: options.showModal !== false } ) if (!values) return false if (isBitRegisterType(group.registerType)) { Object.keys(values.coils || {}).forEach((addressText) => { wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0 }) } else { Object.keys(values.words || {}).forEach((addressText) => { wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF }) } } updateGroups((item) => { if (item.id !== groupId) return item const nextRegisters = item.registers.map((register) => { const rawBytes = registerTypeIsBit(register) ? [] : (isByteAddressedGroup(item) ? getRegisterBytesFromByteCache(register, wordCache) : []) const rawWords = registerTypeIsBit(register) ? [] : (isByteAddressedGroup(item) ? getRegisterWordsFromByteCache(register, wordCache) : getRegisterWordsFromWordCache(register, wordCache)) const rawValue = registerTypeIsBit(register) ? decodeRegisterFromWordCache(register, wordCache) : (isByteAddressedGroup(item) ? decodeRegisterFromByteCache(register, wordCache) : (rawWords ? decodeRegisterValue(register, rawWords) : null)) const displayValue = rawValue === null || rawValue === undefined ? '--' : (registerTypeIsBit(register) ? formatCoilDisplayValue(rawValue) : formatRegisterValue(register, rawValue)) return { ...register, displayValue, inputValue: item.writable ? displayValue : register.inputValue, isDirty: false, rawBytes: rawBytes || [], rawValue, rawWords: rawWords || [] } }) return { ...item, registers: nextRegisters } }) return true } async function writeRegister(groupId, registerIndex) { const group = findGroup(groupId) const register = group && group.registers ? group.registers[registerIndex] : null const slaveAddress = modbusClient.getSharedSlaveAddress() if (!group || !register || slaveAddress === null) return false if (!group.writable) { transport.showCommandAlert('通用Modbus写入', '当前变量为只读') return false } if (group.addressOverflow) { transport.showCommandAlert('通用Modbus写入', '地址范围超出 0xFFFF') return false } const written = isDebugMemoryGroup(group) ? await writeDebugMemoryRegister(group, register, slaveAddress) : null if (!written) { if (!isDebugMemoryGroup(group)) return writeGroup(groupId) return false } updateGroups((item) => { if (item.id !== groupId) return item return { ...item, registers: item.registers.map((currentRegister, currentIndex) => ( currentIndex === registerIndex ? { ...currentRegister, displayValue: written.displayValue, inputValue: written.displayValue, isDirty: false, rawBytes: written.rawBytes, rawValue: written.rawValue, rawWords: written.rawWords } : currentRegister )) } }) return true } async function writeGroup(groupId) { const group = findGroup(groupId) const slaveAddress = modbusClient.getSharedSlaveAddress() const maxPacketLength = resolveMaxPacketLength() if (!group || slaveAddress === null) return false if (!group.writable) { transport.showCommandAlert('通用Modbus写入', '当前寄存器组为只读') return false } if (group.addressOverflow) { transport.showCommandAlert('通用Modbus写入', '寄存器地址范围超出 0xFFFF') return false } const writtenRegisters = [] if (isDebugMemoryGroup(group)) { const snapshots = await writeDebugMemoryGroup(group, slaveAddress) if (!snapshots) return false snapshots.forEach((snapshot) => { writtenRegisters.push(snapshot) }) } else { if (group.registerType === 'coil') { for (let index = 0; index < group.registers.length; index += 1) { const register = group.registers[index] const coilValue = parseCoilValue(getRegisterWriteValueText(register)) if (coilValue === null) { transport.showCommandAlert('通用Modbus写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`) return false } const response = await modbusClient.writeSingleCoil( slaveAddress, group.startAddress + index, !!coilValue, register.name || group.name || '通用Modbus写入', 'generic-modbus-coil-write', { maxFrameBytes: maxPacketLength } ) if (!response) return false writtenRegisters.push({ rawBytes: [], rawValue: coilValue, rawWords: [], displayValue: formatCoilDisplayValue(coilValue) }) } } else { let words try { words = group.isStructLayout ? getGroupEncodedWords(group) : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0) if (!group.isStructLayout) { for (let index = 0; index < group.registers.length; index += 1) { const register = group.registers[index] const registerWords = getRegisterEncodedWords(register) if (!Array.isArray(registerWords) || !registerWords.length) { throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`) } const dataType = getDataType(register.dataType).key const relativeAddress = Math.max(0, register.address - group.startAddress) if (isByteRegister(dataType)) { const byteValue = Number(registerWords[0]) & 0xFF const currentWord = words[relativeAddress] || 0 words[relativeAddress] = register.byteOffset === 0 ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF) : (((currentWord & 0xFF00) | byteValue) & 0xFFFF) } else { for (let offset = 0; offset < register.registerCount; offset += 1) { words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF } } } } } catch (error) { transport.showCommandAlert('通用Modbus写入', error.message || '寄存器组没有有效写入值') return false } const writtenWordCache = words.reduce((cache, word, offset) => { cache[group.startAddress + offset] = word return cache }, {}) group.registers.forEach((register) => { const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || [] const rawValue = decodeRegisterValue(register, rawWords) const displayValue = formatRegisterValue(register, rawValue) writtenRegisters.push({ rawWords, rawValue, displayValue }) }) const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, maxPacketLength) const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity) let cursor = 0 for (const span of spans) { const spanWords = words.slice(cursor, cursor + span.quantity) cursor += span.quantity const response = await modbusClient.writeMultipleRegisters( slaveAddress, span.address, spanWords, group.name || '通用Modbus写入', 'generic-modbus-write', { maxFrameBytes: maxPacketLength } ) if (!response) return false } } } updateGroups((item) => { if (item.id !== groupId) return item let writtenIndex = 0 return { ...item, registers: item.registers.map((register) => { const written = writtenRegisters[writtenIndex] || {} writtenIndex += 1 const hasDisplayValue = Object.prototype.hasOwnProperty.call(written, 'displayValue') const hasRawBytes = Object.prototype.hasOwnProperty.call(written, 'rawBytes') const hasRawValue = Object.prototype.hasOwnProperty.call(written, 'rawValue') const hasRawWords = Object.prototype.hasOwnProperty.call(written, 'rawWords') return { ...register, displayValue: hasDisplayValue ? written.displayValue : register.displayValue, inputValue: hasDisplayValue ? written.displayValue : register.inputValue, isDirty: false, rawBytes: hasRawBytes ? written.rawBytes : register.rawBytes, rawValue: hasRawValue ? written.rawValue : register.rawValue, rawWords: hasRawWords ? written.rawWords : register.rawWords } }) } }) return true } module.exports = { DATA_TYPE_OPTIONS, REGISTER_TYPE_OPTIONS, addGroupFromConfig, completeStructInstanceGroups, completeStructInstanceGroupsWithStructFile, completeStructInstanceGroupsWithStructSource, getState, importJsonFromMessageFile, init, parseStructDefinition, queryCodeInfoBlock, readGroup, removeGroup, reorderRegister, saveJsonToChat, setGroupDeleteVisible, setGroupExpanded, subscribe, updateGroupConfig, updateRegister, updateRegisterValue, validateRegisterInputValue, writeRegister, writeGroup }