const { isCancelError, loadGroupsFromMessageFile, saveGroupsToChat } = require('./persistence.js') const { completeStructInstanceGroups, mergeImportedGroups, parseStructDefinition } = require('./imports.js') const storageAccessCodeInfo = require('../storage-access/code-info-sync.js') const parameterGroupIo = require('./io.js') const settingsService = require('../../store/settings-store.js') const store = require('./store.js') const { loadSelectedFile } = require('../../repositories/file.js') const transport = require('../../transport/ble-core.js') const { DATA_TYPE_OPTIONS, MAX_MODBUS_ADDRESS, MAX_STORAGE_ADDRESS, REGISTER_TYPE_OPTIONS, cloneImportedGroup, getDataType, getRegisterWriteValueText, isAddressRangeOverflow, isStorageSourceLockedGroup, isStorageSourceLockedRegister, normalizeGroup, normalizeGroupConfig, validateRegisterValue } = require('../../domain/parameter-groups/model.js') const { normalizeEnumOptions, parseEnumValueText, parseNumberText } = require('../../domain/parameter-groups/value-codec.js') const { findGroup, getGroups, getState, init, setGroups, setStorageCodeInfo, switchProtocolMode, subscribe, updateGroups } = store let initialized = false function getActiveProtocolMode() { return settingsService.getState().protocolMode } function isStorageAccessProtocolMode(protocolMode) { return settingsService.isStorageAccessProtocol(protocolMode) } function getGroupStorageAddressWidth(group = {}) { const codeInfoContext = group.codeInfoContext || {} const explicitWidth = group.sourceAddressWidth || group.storageAddressWidth || group.addressWidth || codeInfoContext.storageAddressWidth || codeInfoContext.addressWidth || codeInfoContext.codeInfoAddressWidth const explicitByteLength = group.sourceAddressByteLength || group.addressByteLength || codeInfoContext.sourceAddressByteLength || codeInfoContext.addressByteLength const width = Number(explicitWidth) if (width === 16 || width === 2) return 16 if (width === 32 || width === 4) return 32 const byteLength = Number(explicitByteLength) if (byteLength === 2) return 16 if (byteLength === 4) return 32 const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase() if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return 32 return Math.floor(Number(group.startAddress) || 0) > MAX_MODBUS_ADDRESS ? 32 : 16 } function getGroupStorageAddressMax(group = {}) { return getGroupStorageAddressWidth(group) === 16 ? MAX_MODBUS_ADDRESS : MAX_STORAGE_ADDRESS } function isGroupAddressRangeOverflow(group = {}, protocolMode = getActiveProtocolMode()) { if (!isStorageAccessProtocolMode(protocolMode)) { return isAddressRangeOverflow(group.startAddress, group.quantity) } const startAddress = Math.max(0, Math.floor(Number(group.startAddress) || 0)) const addressSpan = Math.max(1, Math.floor(Number(group.byteLength || group.sourceByteLength || group.quantity) || 1)) const maxAddress = getGroupStorageAddressMax(group) return startAddress + addressSpan - 1 > maxAddress } function getAddressOverflowText(protocolMode = getActiveProtocolMode(), group = {}) { if (isStorageAccessProtocolMode(protocolMode)) { return getGroupStorageAddressMax(group) === MAX_MODBUS_ADDRESS ? '地址范围超出 0xFFFF' : '地址范围超出 0xFFFFFFFF' } return '地址范围超出 0xFFFF' } function isUnconfiguredRegister(register = {}) { return getDataType(register.dataType).key === 'raw' } function getUnconfiguredRegisterName(register = {}, index = 0) { return register.displayName || register.name || `变量 ${index + 1}` } function findUnconfiguredRegister(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const index = registers.findIndex(isUnconfiguredRegister) return index >= 0 ? { index, register: registers[index] } : null } function isEnumSourceText(value) { const text = String(value || '').trim().toLowerCase() return text === 'enum' || /^enum\b/.test(text) || /\benum\b/.test(text) } function isEnumLikeRegister(group = {}, register = {}) { if (String(register.enumName || '').trim()) return true if (normalizeEnumOptions(register).length) return true return [ group.sourceEntryKind, group.sourceElementType, group.sourceValueType, register.sourceEntryKind, register.sourceElementType, register.sourceValueType ].some(isEnumSourceText) } function enumOptionMatchesValue(option, value) { if (value === null || value === undefined) return false if (typeof value === 'bigint') { const optionValue = Number(option && option.value) if (!Number.isFinite(optionValue)) return false return BigInt(Math.trunc(optionValue)) === value } return Number(option && option.value) === Number(value) } function validateEnumWriteRegister(group = {}, register = {}, context = {}) { if (!isEnumLikeRegister(group, register)) return true const enumOptions = normalizeEnumOptions(register) if (!enumOptions.length) { if (!context.missingEnumWarningShown) { transport.showCommandAlert('枚举写入', '未加载结构体/枚举定义,无法校验枚举值') context.missingEnumWarningShown = true } return true } const valueText = getRegisterWriteValueText(register).trim() if (!valueText || valueText === '--') return true const enumValue = parseEnumValueText(register, valueText) const parsedValue = enumValue === null ? parseNumberText(valueText, register.dataType) : enumValue const exists = enumOptions.some((option) => enumOptionMatchesValue(option, parsedValue)) if (exists) return true transport.showCommandAlert('枚举写入', '写入值不在枚举定义中,已取消写入') return false } function validateEnumWriteGroup(group = {}) { const context = { missingEnumWarningShown: false } const registers = Array.isArray(group.registers) ? group.registers : [] return registers.every((register) => validateEnumWriteRegister(group, register, context)) } function initParameterGroups() { settingsService.init() init(getActiveProtocolMode()) switchProtocolMode(getActiveProtocolMode(), { notify: false }) if (initialized) return settingsService.subscribe((settingsState) => { switchProtocolMode(settingsState.protocolMode) }) initialized = true } async function importJsonFromMessageFile() { try { const importedGroups = (await loadGroupsFromMessageFile()).map(normalizeGroup) if (!importedGroups.length) throw new Error('JSON 中没有可导入的寄存器组') const merged = mergeImportedGroups(getGroups(), importedGroups) setGroups(merged.groups) return merged.changedCount || 0 } catch (error) { const message = error && error.message ? error.message : '导入参数组配置失败' transport.showCommandAlert('参数组导入', message) return 0 } } function completeStructInstanceGroupsWithStructSource(sourceText, options = {}) { const completed = completeStructInstanceGroups(getGroups(), sourceText, options) if (!completed.completedCount) { throw new Error('没有找到可匹配的结构体实例') } setGroups(completed.groups) return completed } function mergeImportedGroupsIntoState(importedGroups = [], options = {}) { const normalizedGroups = importedGroups.map(cloneImportedGroup).map(normalizeGroup) const merged = mergeImportedGroups(getGroups(), normalizedGroups, options) if (normalizedGroups.length) { setGroups(merged.groups) } return merged } function clearStorageAccessGroups() { setGroups([], { protocolMode: settingsService.PROTOCOL_MODE.STORAGE_ACCESS }) setStorageCodeInfo(null) return true } async function completeStructInstanceGroupsWithStructFile(options = {}) { try { const file = await loadSelectedFile('auto', { 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, enumCount: 0, skippedCount: 0, structCount: 0, variableCount: 0 } } } async function saveJsonToChat() { try { const result = await saveGroupsToChat(getGroups()) return result && result.count ? result : { count: 0 } } catch (error) { const message = error && error.message ? error.message : '保存参数组配置失败' if (!isCancelError(error)) { transport.showCommandAlert('参数组保存', message) } return { count: 0 } } } async function syncFromStorageAccessCodeInfo(options = {}) { const result = await storageAccessCodeInfo.syncCodeInfo(options) if (!result || !result.ok) return result setStorageCodeInfo(result.codeInfo) settingsService.setStorageAccessDefaultEndian(result.codeInfoMemoryEndian || result.codeInfo && result.codeInfo.memoryEndian) const merged = mergeImportedGroupsIntoState(result.importedGroups || [], { preserveExistingRemarks: true, preserveExistingPollEnabled: true, preserveExistingStructLayout: true }) return { ...result, addedGroups: merged.addedGroupCount, addedRegisters: merged.addedRegisterCount, updatedGroups: merged.updatedGroupCount, updatedRegisters: merged.updatedRegisterCount } } function addGroupFromConfig(config = {}) { const protocolMode = getActiveProtocolMode() let groupConfig try { groupConfig = normalizeGroupConfig({ ...(isStorageAccessProtocolMode(protocolMode) ? { addressUnit: 'byte', sourceMemoryArea: config.sourceMemoryArea || 'XDATA' } : {}), ...config }) } catch (error) { transport.showCommandAlert('参数组添加', error.message || '寄存器组配置无效') return null } if (isGroupAddressRangeOverflow(groupConfig, protocolMode)) { transport.showCommandAlert('参数组添加', getAddressOverflowText(protocolMode, groupConfig)) 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('参数组添加', getAddressOverflowText(protocolMode, group)) return null } setGroups(getGroups().concat(group)) return group } function updateGroupConfig(groupId, config = {}) { const protocolMode = getActiveProtocolMode() const group = findGroup(groupId) if (!group) return null if (isStorageSourceLockedGroup(group)) { return updateGroupPollEnabled(groupId, config.pollEnabled) } let nextConfig try { nextConfig = normalizeGroupConfig({ ...group, ...config }) } catch (error) { transport.showCommandAlert('参数组更新', error.message || '寄存器组配置无效') return null } if (isGroupAddressRangeOverflow(nextConfig, protocolMode)) { transport.showCommandAlert('参数组更新', getAddressOverflowText(protocolMode, nextConfig)) return null } const registers = Array.isArray(config.registers) ? config.registers : group.registers const updatedGroup = normalizeGroup({ ...group, ...nextConfig, registers }) if (updatedGroup.addressOverflow) { transport.showCommandAlert('参数组更新', getAddressOverflowText(protocolMode, updatedGroup)) return null } setGroups(getGroups().map((item) => ( item.id === groupId ? updatedGroup : item ))) return updatedGroup } function updateGroupPollEnabled(groupId, pollEnabled) { let updatedGroup = null updateGroups((group) => { if (group.id !== groupId) return group updatedGroup = normalizeGroup({ ...group, pollEnabled: pollEnabled === false ? false : true }) return updatedGroup }) return updatedGroup } 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) { setGroups(getGroups().filter((group) => group.id !== groupId)) } function reorderRegister(groupId, fromIndex, toIndex) { const group = findGroup(groupId) if (!group) return null if (group.isStructLayout || group.sourceMetadataLocked) 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 }) setGroups(getGroups().map((item) => ( item.id === groupId ? updatedGroup : item ))) return updatedGroup } function updateRegister(groupId, registerIndex, changedData) { updateGroups((group) => { if (group.id !== groupId) return group const register = group.registers[registerIndex] const safeChangedData = isStorageSourceLockedRegister(group, register) ? Object.keys(changedData || {}).reduce((data, key) => { if (key !== 'name' && key !== 'dataType' && key !== 'textByteLength') { data[key] = changedData[key] } return data }, {}) : changedData const shouldResetReadState = Object.prototype.hasOwnProperty.call(safeChangedData, 'dataType') || Object.prototype.hasOwnProperty.call(safeChangedData, 'textByteLength') return { ...group, registers: group.registers.map((register, currentIndex) => ( currentIndex === registerIndex ? { ...register, ...(shouldResetReadState ? { rawBytes: [], rawValue: null, rawWords: [] } : {}), ...safeChangedData } : 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) } async function readGroup(groupId, options = {}) { const protocolMode = options.protocolMode || getActiveProtocolMode() const group = findGroup(groupId, protocolMode) if (!group) return false if (group.addressOverflow) { transport.showCommandAlert('参数组读取', getAddressOverflowText(protocolMode, group)) return false } const readResult = await parameterGroupIo.readGroup(group, { ...options, useStorageAccess: isStorageAccessProtocolMode(protocolMode) }) if (!readResult) return false updateGroups((item) => { if (item.id !== groupId) return item return readResult.applyToGroup(item) }, { protocolMode }) return true } async function writeRegister(groupId, registerIndex) { const protocolMode = getActiveProtocolMode() const group = findGroup(groupId, protocolMode) const register = group && group.registers ? group.registers[registerIndex] : null if (!group || !register) return false if (!group.writable) { transport.showCommandAlert('参数组写入', '当前变量为只读') return false } if (group.addressOverflow) { transport.showCommandAlert('参数组写入', getAddressOverflowText(protocolMode, group)) return false } if (isUnconfiguredRegister(register)) { transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(register, registerIndex)} 需要先配置数据类型`) return false } if (!validateEnumWriteRegister(group, register)) return false const writeResult = await parameterGroupIo.writeRegister(group, registerIndex, { useStorageAccess: isStorageAccessProtocolMode(protocolMode) }) if (!writeResult) return false updateGroups((item) => { if (item.id !== groupId) return item return writeResult.applyToGroup(item) }, { protocolMode }) return true } async function writeGroup(groupId, options = {}) { const protocolMode = options.protocolMode || getActiveProtocolMode() const group = findGroup(groupId, protocolMode) if (!group) return false if (!group.writable) { transport.showCommandAlert('参数组写入', '当前寄存器组为只读') return false } if (group.addressOverflow) { transport.showCommandAlert('参数组写入', getAddressOverflowText(protocolMode, group)) return false } const unconfigured = findUnconfiguredRegister(group) if (unconfigured) { transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(unconfigured.register, unconfigured.index)} 需要先配置数据类型`) return false } if (!validateEnumWriteGroup(group)) return false const writeResult = await parameterGroupIo.writeGroup(group, { ...options, useStorageAccess: isStorageAccessProtocolMode(protocolMode) }) if (!writeResult) return false updateGroups((item) => { if (item.id !== groupId) return item return writeResult.applyToGroup(item) }, { protocolMode }) return true } module.exports = { DATA_TYPE_OPTIONS, REGISTER_TYPE_OPTIONS, addGroupFromConfig, clearStorageAccessGroups, completeStructInstanceGroups, completeStructInstanceGroupsWithStructFile, completeStructInstanceGroupsWithStructSource, getState, importJsonFromMessageFile, init: initParameterGroups, mergeImportedGroups: mergeImportedGroupsIntoState, parseStructDefinition, readGroup, removeGroup, reorderRegister, saveJsonToChat, setGroupDeleteVisible, setGroupExpanded, subscribe, syncFromStorageAccessCodeInfo, updateGroupConfig, updateGroupPollEnabled, updateRegister, updateRegisterValue, validateRegisterInputValue, writeRegister, writeGroup }