const { getDataType, isStorageStructGroup, normalizeGroup } = require('../../domain/parameter-groups/model.js') const { parseStructCatalog, parseStructDefinition: parseStructDefinitionSource } = require('../../domain/parameter-groups/struct-parser.js') 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(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '') .replace(/^_+/, '') .replace(/[^A-Za-z0-9]/g, '') .toLowerCase() } function getStorageStructTypeName(group = {}) { return String(group.sourceSymbolType || group.sourceSymbolName || group.name || '') } function isStructCodeInfoEntry(group = {}) { const entryKind = String(group.sourceEntryKind || '').trim().toLowerCase() return !entryKind || entryKind === 'struct' } function isVariableCodeInfoEntry(group = {}) { return String(group.sourceEntryKind || '').trim().toLowerCase() === 'variable' } function structDefinitionNameMatches(group = {}, structInfo = {}) { const expectedName = normalizeSymbolText(getStorageStructTypeName(group)) const structName = normalizeSymbolText(structInfo.name) return !!expectedName && !!structName && expectedName === structName } function findStructCompletion(group, catalog) { if (isStorageStructGroup(group) && isStructCodeInfoEntry(group)) { const matchedStruct = catalog.structs.find((structInfo) => structDefinitionNameMatches(group, structInfo)) return matchedStruct ? { name: group.sourceSymbolName || group.name, registers: matchedStruct.registers, structName: matchedStruct.name } : null } 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 getEnumLookupNames(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const names = [ group.sourceSymbolType, group.sourceSymbolName, group.name ] registers.forEach((register) => { names.push(register.sourceSymbolType, register.sourceSymbolName, register.name) }) return names.map(normalizeSymbolText).filter(Boolean) } function findEnumCompletion(group, catalog = {}) { const enums = Array.isArray(catalog.enums) ? catalog.enums : [] if (!isVariableCodeInfoEntry(group) || !enums.length) return null const names = getEnumLookupNames(group) if (!names.length) return null const enumVariablesByName = catalog.enumVariablesByName || {} for (const name of names) { const variableEnum = enumVariablesByName[name] if (variableEnum) return variableEnum } return enums.find((enumInfo) => ( [enumInfo.name, enumInfo.typedefName, enumInfo.tagName] .concat(enumInfo.typeNames || []) .map(normalizeSymbolText) .filter(Boolean) .some((name) => names.indexOf(name) >= 0) )) || null } function getIntegerDataTypeForByteLength(byteLength, fallback = 'uint16_t') { const length = Number(byteLength) if (length === 1) return 'uint8_t' if (length === 2) return 'uint16_t' if (length === 4) return 'uint32_t' return fallback } function cloneEnumOptions(enumInfo) { return (Array.isArray(enumInfo && enumInfo.options) ? enumInfo.options : []).map((option) => ({ label: option.label || option.name, name: option.name || option.label, value: Number(option.value) || 0 })) } function completeEnumVariableGroup(group, enumInfo) { if (!enumInfo) return group const enumOptions = cloneEnumOptions(enumInfo) if (!enumOptions.length) return group const registers = (Array.isArray(group.registers) ? group.registers : []).map((register) => ({ ...register, dataType: getIntegerDataTypeForByteLength( register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength, register.dataType || enumInfo.dataType ), enumName: enumInfo.name, enumOptions, sourceSymbolType: enumInfo.name || register.sourceSymbolType })) return normalizeGroup({ ...group, registers }) } function createCompletedRegisters(group, completion) { const existingRemarksByByteStart = (Array.isArray(group.registers) ? group.registers : []).reduce((remarks, register) => { const byteStart = Number(register && register.byteStart) const remark = String(register && register.remark ? register.remark : '').trim() if (Number.isFinite(byteStart) && remark) remarks[Math.floor(byteStart)] = remark return remarks }, {}) return completion.registers.map((register) => { const sourceAddress = (Number(group.sourceAddress) || Number(group.startAddress) || 0) + getRegisterByteStart(register) return { ...register, isStructField: true, remark: register.remark || existingRemarksByByteStart[Math.floor(Number(register.byteStart) || 0)] || '', sourceAddress, sourceAddressByteLength: group.sourceAddressByteLength, sourceAddressText: formatAddress(sourceAddress, group.sourceAddressWidth), sourceAddressWidth: group.sourceAddressWidth, sourceEntryKind: group.sourceEntryKind, 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 if (isVariableCodeInfoEntry(group)) { const enumInfo = findEnumCompletion(group, catalog) if (!enumInfo) return group completedCount += 1 return completeEnumVariableGroup(group, enumInfo) } if (!isStructCodeInfoEntry(group)) 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, enumCount: Array.isArray(catalog.enums) ? catalog.enums.length : 0, variableCount: Object.keys(catalog.variablesByName).length } } function formatAddress(address, addressWidth) { const numberValue = Math.max(0, Math.floor(Number(address) || 0)) const length = Number(addressWidth) === 32 || numberValue > 0xFFFF ? 8 : 4 return `0x${numberValue.toString(16).toUpperCase().padStart(length, '0')}` } function normalizeDuplicateText(value) { return String(value === undefined || value === null ? '' : value) .trim() .toLowerCase() } function normalizeStructMatchText(value) { return String(value === undefined || value === null ? '' : value) .trim() .replace(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '') .replace(/^struct\s+/i, '') .replace(/\s+#\d+$/i, '') .replace(/^_+/, '') .replace(/[^A-Za-z0-9]/g, '') .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 || '') const addressKey = normalizeAddressKey( group.sourceAddress !== undefined ? group.sourceAddress : group.startAddress, group.sourceAddressText || group.startAddressText ) if (area && symbolName && addressKey) return ['group', area, symbolName, addressKey].join('|') if (area && symbolName) return ['group', area, symbolName].join('|') 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 getStructMatchNames(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const names = [ group.sourceSymbolName, group.sourceSymbolType, group.name, group.displayName ] registers.forEach((register) => { names.push(register.sourceSymbolType, register.sourceSymbolName) }) return names .map(normalizeStructMatchText) .filter(Boolean) .filter((name, index, list) => list.indexOf(name) === index) } function structsMatchByName(existingGroup = {}, incomingGroup = {}) { const existingNames = getStructMatchNames(existingGroup) const incomingNames = getStructMatchNames(incomingGroup) return existingNames.some((name) => incomingNames.indexOf(name) >= 0) } function getGroupByteLengthCandidates(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const candidates = [ group.sourceByteLength, group.byteLength, group.structByteLength, getRegistersByteLength(registers) ] registers.forEach((register) => { candidates.push(register.structByteLength) }) return candidates .map((value) => Number(value)) .filter((value) => Number.isFinite(value) && value > 0) .map((value) => Math.floor(value)) .filter((value, index, list) => list.indexOf(value) === index) } function structsMatchByByteLength(existingGroup = {}, incomingGroup = {}) { const existingLengths = getGroupByteLengthCandidates(existingGroup) const incomingLengths = getGroupByteLengthCandidates(incomingGroup) return existingLengths.some((length) => incomingLengths.indexOf(length) >= 0) } function structsMatchByLocation(existingGroup = {}, incomingGroup = {}) { const existingArea = normalizeDuplicateText(existingGroup.sourceMemoryArea || '') const incomingArea = normalizeDuplicateText(incomingGroup.sourceMemoryArea || '') const existingAddress = normalizeAddressKey( existingGroup.sourceAddress !== undefined ? existingGroup.sourceAddress : existingGroup.startAddress, existingGroup.sourceAddressText || existingGroup.startAddressText ) const incomingAddress = normalizeAddressKey( incomingGroup.sourceAddress !== undefined ? incomingGroup.sourceAddress : incomingGroup.startAddress, incomingGroup.sourceAddressText || incomingGroup.startAddressText ) if (existingArea && incomingArea && existingArea !== incomingArea) return false if (existingAddress && incomingAddress && existingAddress !== incomingAddress) return false return true } function isIncomingPlaceholderStructGroup(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] return group.layout === 'struct' && registers.length > 0 && registers.every((register) => !!register.isPlaceholderByteField) } function hasImportedStructRegisters(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] return group.layout === 'struct' && registers.length > 0 && registers.some((register) => !register.isPlaceholderByteField) } function canPreserveExistingStructLayout(existingGroup, incomingGroup, options = {}) { return options.preserveExistingStructLayout && hasImportedStructRegisters(existingGroup) && isIncomingPlaceholderStructGroup(incomingGroup) && structsMatchByName(existingGroup, incomingGroup) && structsMatchByByteLength(existingGroup, incomingGroup) && structsMatchByLocation(existingGroup, incomingGroup) } function getRegisterByteStart(register = {}) { const byteStart = Number(register.byteStart) return Number.isFinite(byteStart) ? Math.max(0, Math.floor(byteStart)) : 0 } function mergePreservedStructRegister(register = {}, incomingGroup = {}) { const byteStart = getRegisterByteStart(register) const sourceAddress = (Number(incomingGroup.sourceAddress) || Number(incomingGroup.startAddress) || 0) + byteStart const sourceSymbolName = incomingGroup.sourceSymbolName || register.sourceSymbolName const sourceSymbolType = incomingGroup.sourceSymbolType || register.sourceSymbolType || sourceSymbolName return { ...register, rawBytes: [], rawValue: null, rawWords: [], sourceAddress, sourceAddressByteLength: incomingGroup.sourceAddressByteLength || register.sourceAddressByteLength, sourceAddressText: formatAddress(sourceAddress, incomingGroup.sourceAddressWidth || register.sourceAddressWidth), sourceAddressWidth: incomingGroup.sourceAddressWidth || register.sourceAddressWidth, sourceEntryKind: incomingGroup.sourceEntryKind, sourceMemoryArea: incomingGroup.sourceMemoryArea, sourceMemoryClass: incomingGroup.sourceMemoryClass, sourceSymbolName, sourceSymbolType } } function resolveMergedPollEnabled(existingGroup = {}, incomingGroup = {}, options = {}) { if (options.preserveExistingPollEnabled && existingGroup.pollEnabled === false) return false return incomingGroup.pollEnabled === false ? false : true } function mergePreservedStructGroupState(existingGroup, incomingGroup, options = {}) { const preservedRegisters = (Array.isArray(existingGroup.registers) ? existingGroup.registers : []) .map((register) => mergePreservedStructRegister(register, incomingGroup)) return { ...incomingGroup, deleteVisible: false, expanded: existingGroup.expanded === true, id: existingGroup.id, pollEnabled: resolveMergedPollEnabled(existingGroup, incomingGroup, options), quantity: preservedRegisters.length, registers: preservedRegisters } } function findPreservableStructGroupIndex(groups = [], incomingGroup = {}, preferredIndex, options = {}) { if (preferredIndex !== undefined && canPreserveExistingStructLayout(groups[preferredIndex], incomingGroup, options)) { return preferredIndex } if (!options.preserveExistingStructLayout || !isIncomingPlaceholderStructGroup(incomingGroup)) return undefined return groups.findIndex((group, index) => ( index !== preferredIndex && canPreserveExistingStructLayout(group, incomingGroup, options) )) } function mergeImportedRegisterState(existingRegister, incomingRegister, options = {}) { if (!existingRegister) return incomingRegister const incomingRemark = incomingRegister.remark const shouldPreserveRemark = options.preserveExistingRemarks && !String(incomingRemark === undefined || incomingRemark === null ? '' : incomingRemark).trim() return { ...incomingRegister, id: existingRegister.id, inputValue: incomingRegister.inputValue !== undefined && incomingRegister.inputValue !== null ? incomingRegister.inputValue : existingRegister.inputValue, remark: shouldPreserveRemark ? existingRegister.remark : (incomingRemark !== undefined && incomingRemark !== null ? incomingRemark : existingRegister.remark), rawBytes: [], rawValue: null, rawWords: [] } } function mergeImportedGroupState(existingGroup, incomingGroup, options = {}) { if (!existingGroup) return incomingGroup if (canPreserveExistingStructLayout(existingGroup, incomingGroup, options)) { return mergePreservedStructGroupState(existingGroup, incomingGroup, options) } const existingRegisters = Array.isArray(existingGroup.registers) ? existingGroup.registers : [] const incomingRegisters = Array.isArray(incomingGroup.registers) ? incomingGroup.registers : [] return { ...incomingGroup, deleteVisible: false, expanded: existingGroup.expanded === true, id: existingGroup.id, pollEnabled: resolveMergedPollEnabled(existingGroup, incomingGroup, options), registers: incomingRegisters.map((incomingRegister, index) => mergeImportedRegisterState( existingRegisters[index], incomingRegister, options )) } } function mergeAggregateImportedGroup(nextGroups, incomingGroup, indexes, options = {}) { 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, options) 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 }, options)) } 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 = [], options = {}) { 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, options) 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 const preservableStructGroupIndex = findPreservableStructGroupIndex( nextGroups, incomingGroup, existingGroupIndex, options ) const targetGroupIndex = preservableStructGroupIndex >= 0 ? preservableStructGroupIndex : existingGroupIndex if (targetGroupIndex !== undefined) { const existingGroup = nextGroups[targetGroupIndex] nextGroups[targetGroupIndex] = normalizeGroup(mergeImportedGroupState(existingGroup, incomingGroup, options)) 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 parseStructDefinition(sourceText) { return parseStructDefinitionSource(sourceText) } module.exports = { completeStructInstanceGroups, getRegistersByteLength, mergeImportedGroups, parseStructDefinition }