const { clampInteger, createId, normalizeTextValue, padHex } = require('../../utils/base-utils.js') const { BYTE_ADDRESS_MEMORY_AREAS, DATA_TYPE_OPTIONS, DEFAULT_DATA_TYPE, DEFAULT_REGISTER_TYPE, DEFAULT_TEXT_BYTE_LENGTH, GROUP_LAYOUT_REGISTER, GROUP_LAYOUT_STRUCT, MAX_MODBUS_ADDRESS, MAX_PARAMETER_GROUP_ITEMS, REGISTER_TYPE_OPTIONS, SOURCE_GROUP_FIELDS, SOURCE_REGISTER_FIELDS, STRUCT_REGISTER_FIELDS } = require('./constants.js') const { formatFixedValue } = require('../../utils/number-format.js') const { evaluateValueFormula } = require('./value-formula.js') const { alignEvenByteLength, decodeRegisterValue, formatCoilDisplayValue, formatRawByteText, formatRawByteTextWithDefault, formatRawWordText, formatRegisterValue, getDataType, getDataTypeIndex, getRegisterByteLength, getRegisterValueTypeLabel, getRegisterWordCount, getRegisterWordCountAtOffset, isBitFieldRegister, isBitRegisterType, isByteRegister, isTextRegister, normalizeBitOffset, normalizeBitWidth, normalizeTextByteLength, parseCoilValue, registerTypeIsBit, supportsRange, supportsUnit } = require('./value-codec.js') const { decodeRegisterFromByteCache, decodeRegisterFromWordCache, getGroupEncodedBytes, getGroupEncodedWords, getRegisterBytesFromByteCache, getRegisterEncodedBytes, getRegisterEncodedWords, getRegisterWordsFromByteCache, getRegisterWordsFromWordCache, getRegisterWriteValueText, splitWordSpans, validateRegisterValue } = require('./register-io.js') function normalizeAddress(value, fallback = 0) { if (typeof value === 'number') { return Number.isFinite(value) ? clampInteger(value, 0, MAX_MODBUS_ADDRESS, fallback) : fallback } const text = String(value === undefined || value === null ? '' : value).trim() if (!text) return fallback const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text if (/^[0-9A-F]+$/i.test(hexText)) { const parsedHex = parseInt(hexText, 16) return Number.isFinite(parsedHex) ? clampInteger(parsedHex, 0, MAX_MODBUS_ADDRESS, fallback) : fallback } const numberValue = Number(text) return Number.isFinite(numberValue) ? clampInteger(numberValue, 0, MAX_MODBUS_ADDRESS, fallback) : fallback } function parseConfigAddress(value) { if (typeof value === 'number') { return clampInteger(value, 0, MAX_MODBUS_ADDRESS, 0) } const text = String(value === undefined || value === null ? '' : value).trim() const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text if (!/^[0-9A-F]{1,4}$/i.test(hexText)) { throw new Error('寄存器起始地址无效') } return parseInt(hexText, 16) } function parseConfigQuantity(value, maxQuantity) { const text = String(value === undefined || value === null ? '' : value).trim() const quantity = Number(text) if (!Number.isInteger(quantity) || quantity < 1 || quantity > maxQuantity) { throw new Error(`寄存器数量需为 1 - ${maxQuantity}`) } return quantity } function getRegisterType(typeKey) { return REGISTER_TYPE_OPTIONS.find((item) => item.key === typeKey) || REGISTER_TYPE_OPTIONS[0] } function getRegisterTypeIndex(typeKey) { return Math.max(0, REGISTER_TYPE_OPTIONS.findIndex((item) => item.key === getRegisterType(typeKey).key)) } function isStructLayout(layout) { return layout === GROUP_LAYOUT_STRUCT } function padWordHex(value) { return Number(value || 0).toString(16).toUpperCase().padStart(4, '0') } function formatAddressRange(startAddress, wordCount) { const address = normalizeAddress(startAddress, 0) const count = Math.max(1, Number(wordCount) || 1) const endAddress = address + count - 1 const safeEndAddress = Math.min(endAddress, MAX_MODBUS_ADDRESS) const overflowText = endAddress > MAX_MODBUS_ADDRESS ? '+' : '' if (count <= 1) return `0x${padWordHex(address)}` return `0x${padWordHex(address)}-0x${padWordHex(safeEndAddress)}${overflowText}` } function isByteAddressedGroup(group = {}) { const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase() return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || BYTE_ADDRESS_MEMORY_AREAS.indexOf(memoryArea) >= 0 } function formatRegisterAddressText(address, byteOffset, byteLength, registerType) { if (isBitRegisterType(registerType)) return `0x${padHex(address)}` if (byteLength === 1) return `0x${padHex(address)}${byteOffset === 0 ? 'H' : 'L'}` return `0x${padHex(address)}` } function formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth) { const byteText = formatRegisterAddressText(address, byteOffset, 1, DEFAULT_REGISTER_TYPE) const startBit = normalizeBitOffset(bitOffset) const endBit = startBit + normalizeBitWidth(bitWidth) - 1 return endBit === startBit ? `${byteText}.b${startBit}` : `${byteText}.b${startBit}..${endBit}` } function isAddressRangeOverflow(startAddress, wordCount) { const address = normalizeAddress(startAddress, 0) const count = Math.max(1, Number(wordCount) || 1) return address + count - 1 > MAX_MODBUS_ADDRESS } function getMaxQuantity() { return MAX_PARAMETER_GROUP_ITEMS } function getRegisterSavedValue(register) { if (register.inputValue !== undefined && register.inputValue !== null) return normalizeTextValue(register.inputValue) if (register.value !== undefined && register.value !== null) return normalizeTextValue(register.value) return null } function normalizeRegisterDataType(register, registerType) { if (isBitRegisterType(registerType)) return DEFAULT_DATA_TYPE return getDataType(register.dataType || register.type || DEFAULT_DATA_TYPE).key } 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 createRegisterSourceMetaText(register) { const bitText = isBitFieldRegister(register) ? `bit${normalizeBitOffset(register.bitOffset)}:${normalizeBitWidth(register.bitWidth)}` : '' const parts = [ register.sourceMemoryArea, register.sourceAddressText, bitText, register.sourceSymbolType && register.sourceSymbolType !== '---' ? register.sourceSymbolType : '' ].filter(Boolean) return parts.join(' · ') } function createGroupSourceMetaText(group) { const parts = [ group.sourceMemoryArea, group.sourceAddressText, group.sourceSymbolName, group.sourceSegmentModule ].filter(Boolean) return parts.join(' · ') } function formatCompactNumber(value, precision = 4) { const text = formatFixedValue(value, precision) return text === '--' ? '--' : text.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '') } function formatCardMetricValue(value) { if (value === '--') return '--' return formatCompactNumber(value) } function normalizeStorageCodeInfoCard(codeInfo = null) { const hasCodeInfo = !!codeInfo && codeInfo.hasCodeInfo !== false const refVolt = hasCodeInfo ? Number(codeInfo.refVolt) : NaN const card = { alongDiv: hasCodeInfo ? (Number(codeInfo.alongDiv) || 0) : 0, ampGain: hasCodeInfo ? (Number(codeInfo.ampGain) || 0) : '--', busDiv: hasCodeInfo ? (Number(codeInfo.busDiv) || 0) : 0, caveFreq: hasCodeInfo ? (Number(codeInfo.caveFreq) || 0) : '--', chipModel: hasCodeInfo ? (String(codeInfo.chipModel || '').trim() || '--') : '--', model: hasCodeInfo ? (String(codeInfo.model || '').trim() || '--') : '--', refVolt: hasCodeInfo ? (Number.isFinite(refVolt) ? refVolt : ((Number(codeInfo.refVoltRaw) || 0) / 10)) : '--', refVoltRaw: hasCodeInfo ? (Number(codeInfo.refVoltRaw) || 0) : 0, rsShunt: hasCodeInfo ? (Number(codeInfo.rsShunt) || 0) : '--' } const codeInfoContext = hasCodeInfo ? { alongDiv: card.alongDiv, ampGain: card.ampGain, busDiv: card.busDiv, caveFreq: card.caveFreq, refVolt: card.refVolt, rsShunt: card.rsShunt } : {} return { ...card, codeInfoContext, hasCodeInfo, metricItems: [ { text: `${formatCardMetricValue(card.caveFreq)}KHz` }, { text: `${formatCardMetricValue(card.refVolt)}V` }, { text: formatCardMetricValue(card.ampGain) }, { text: `${formatCardMetricValue(card.rsShunt)}mΩ` } ] } } function isStorageStructGroup(group = {}) { return isStructLayout(group.layout) && isByteAddressedGroup(group) && !!String(group.sourceMemoryArea || '').trim() } function getStorageAreaText(group = {}, lowercase = false) { const area = String(group.sourceMemoryArea || '').trim() return lowercase ? area.toLowerCase() : area.toUpperCase() } function stripStorageAreaPrefix(text) { const source = String(text || '').trim() const cleaned = source.replace(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '').trim() return cleaned || source } function formatRegisterStartAddressText(address) { return `0x${padHex(address)}` } function formatStorageHexBytes(bytes = [], byteLength = 1) { const safeLength = Math.max(1, Math.floor(Number(byteLength) || 1)) const source = Array.isArray(bytes) ? bytes : [] return `0x${Array.from({ length: safeLength }, (_, index) => ( (Number(source[index] || 0) & 0xFF).toString(16).toUpperCase().padStart(2, '0') )).join('')}` } function formatStorageRegisterHexValue(register, rawBytes = []) { if (isTextRegister(register.dataType)) { return formatRawByteTextWithDefault(rawBytes, register.byteLength) } return formatStorageHexBytes(rawBytes, register.byteLength) } function normalizeRegister(register, group, index, address, byteOffset = 0) { const registerType = getRegisterType(group.registerType).key const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER const isStorageStruct = isStorageStructGroup(group) const byteAddressed = isByteAddressedGroup(group) const dataType = normalizeRegisterDataType(register, registerType) const bitOffset = normalizeBitOffset(register.bitOffset) const bitWidth = normalizeBitWidth(register.bitWidth) const isBitField = !isBitRegisterType(registerType) && isBitFieldRegister(register) const isPlaceholderByteField = !!register.isPlaceholderByteField const textByteLength = isTextRegister(dataType) ? normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH) : '' const defaultValue = normalizeTextValue(register.defaultValue) const savedValue = getRegisterSavedValue(register) const inputValue = savedValue === null ? defaultValue : savedValue const rawValue = register.rawValue === undefined ? null : register.rawValue const byteLength = isBitRegisterType(registerType) ? 1 : getRegisterByteLength(dataType, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength }) const registerCount = isBitRegisterType(registerType) ? 1 : getRegisterWordCountAtOffset(dataType, byteOffset, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength }) const canShowUnit = !isBitRegisterType(registerType) && !isBitField && supportsUnit(dataType) const rawWords = Array.isArray(register.rawWords) ? register.rawWords.slice(0, registerCount).map((word) => Number(word) & 0xFFFF) : [] const rawBytes = Array.isArray(register.rawBytes) ? register.rawBytes.slice(0, byteLength).map((byte) => Number(byte) & 0xFF) : [] const defaultRawValueText = rawValue === null ? '--' : (isBitRegisterType(registerType) ? formatCoilDisplayValue(rawValue) : (byteAddressed ? formatRawByteText(rawBytes) : formatRawWordText(rawWords))) const rawValueText = isStorageStruct ? formatStorageRegisterHexValue({ ...register, byteLength, dataType }, rawBytes) : defaultRawValueText const displayValue = rawValue === null ? (inputValue.trim() ? inputValue : '--') : formatRegisterValue({ ...register, dataType, byteOffset }, rawValue) const conversionFormula = normalizeTextValue(register.conversionFormula).trim() const conversionResult = conversionFormula && rawValue !== null ? evaluateValueFormula(conversionFormula, rawValue, group.codeInfoContext || {}) : null const convertedDisplayValue = conversionResult && conversionResult.ok ? conversionResult.text : displayValue const displayMetaText = conversionResult && conversionResult.ok ? `raw ${displayValue}` : '' const addressRangeText = isBitRegisterType(registerType) ? `0x${padHex(address)}` : (byteAddressed ? formatAddressRange(address, Math.max(1, byteLength)) : formatAddressRange(address, registerCount)) const addressText = isBitField ? (byteAddressed ? `${formatAddressRange(address, Math.max(1, byteLength))}.b${bitOffset}${bitWidth > 1 ? `..b${bitOffset + bitWidth - 1}` : ''}` : formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth)) : (byteAddressed ? formatAddressRange(address, Math.max(1, byteLength)) : formatRegisterAddressText(address, byteOffset, byteLength, registerType)) const registerStartAddressText = formatRegisterStartAddressText(address) const metaText = isStorageStruct ? `${registerStartAddressText} ${rawValueText}` : `${addressText} ${rawValueText}`.trim() return { address, addressRangeText, addressText, bitOffset: isBitField ? bitOffset : '', bitWidth: isBitField ? bitWidth : '', byteLength, byteLengthText: isBitRegisterType(registerType) ? '1bit' : (isBitField ? (bitWidth === 1 ? `1bit/占${byteLength}B` : `${bitWidth}bit/占${byteLength}B`) : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`)), byteStart: Math.max(0, Math.floor(Number(register.byteStart) || 0)), dataType, dataTypeIndex: getDataTypeIndex(dataType), dataTypeText: getRegisterValueTypeLabel(dataType), defaultValue, displayMetaText, displayValue: convertedDisplayValue, id: register.id || createId('gm-reg'), inputType: isTextRegister(dataType) ? 'text' : 'text', inputValue, isStructField: layout === GROUP_LAYOUT_STRUCT || !!register.isStructField, layout, isBitField, isPlaceholderByteField, isDirty: !!register.isDirty, maxValue: normalizeTextValue(register.maxValue), metaText, minValue: normalizeTextValue(register.minValue), name: register.name || `寄存器 ${index + 1}`, conversionFormula, conversionFormulaErrorText: conversionResult && conversionResult.errorText ? conversionResult.errorText : '', rawValue, rawValueText, rawBytes, rawWords, registerCount, byteOffset, registerType, showDataType: !isBitRegisterType(registerType), showRange: !isBitRegisterType(registerType) && !isBitField && supportsRange(dataType), showTextLength: !isBitRegisterType(registerType) && isTextRegister(dataType), showUnit: canShowUnit, textByteLength, unit: canShowUnit ? normalizeTextValue(register.unit).trim() : '', structByteLength: register.structByteLength, remark: register.remark || '', ...pickFields(register, SOURCE_REGISTER_FIELDS), sourceMetaText: createRegisterSourceMetaText(register) } } function normalizeGroup(group) { const registerType = getRegisterType(group.registerType || group.type || DEFAULT_REGISTER_TYPE) const startAddress = normalizeAddress(group.startAddress, 0) const maxQuantity = getMaxQuantity(registerType.key) const sourceRegisters = Array.isArray(group.registers) ? group.registers : [] const codeInfoContext = group.codeInfoContext || {} const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER const byteAddressed = isByteAddressedGroup(group) const hasExplicitQuantity = group.quantity !== undefined && group.quantity !== null && group.quantity !== '' const quantity = hasExplicitQuantity ? clampInteger(group.quantity, 1, maxQuantity, 1) : clampInteger(sourceRegisters.length || 1, 1, maxQuantity, 1) const baseGroup = { deleteVisible: !!group.deleteVisible, expanded: group.expanded === true, id: group.id || createId('gm-group'), isStructLayout: layout === GROUP_LAYOUT_STRUCT, layout, name: String(group.name || group.groupName || '寄存器组').trim() || '寄存器组', quantity, registerType: registerType.key, startAddress, touchStartX: 0, codeInfoContext, ...pickFields(group, SOURCE_GROUP_FIELDS) } const registers = [] let nextAddress = startAddress let nextByteOffset = 0 for (let index = 0; index < quantity; index += 1) { const sourceRegister = sourceRegisters[index] || {} let normalizedSourceRegister = sourceRegister const dataType = normalizeRegisterDataType(sourceRegister, baseGroup.registerType) const textByteLength = isTextRegister(dataType) ? normalizeTextByteLength(sourceRegister.textByteLength, DEFAULT_TEXT_BYTE_LENGTH) : '' const isBitRegister = isBitRegisterType(baseGroup.registerType) let address = startAddress + index let byteOffset = 0 if (!isBitRegister) { const explicitByteStart = Number(sourceRegister.byteStart) const hasExplicitByteStart = layout === GROUP_LAYOUT_STRUCT && Number.isFinite(explicitByteStart) const byteLength = getRegisterByteLength(dataType, { ...sourceRegister, layout, textByteLength }) if (layout !== GROUP_LAYOUT_STRUCT && !isByteRegister(dataType) && nextByteOffset % 2 !== 0) { nextByteOffset += 1 } const currentByteStart = hasExplicitByteStart ? Math.max(0, Math.floor(explicitByteStart)) : nextByteOffset address = byteAddressed ? startAddress + currentByteStart : startAddress + Math.floor(currentByteStart / 2) byteOffset = byteAddressed ? 0 : currentByteStart % 2 normalizedSourceRegister = { ...sourceRegister, byteStart: currentByteStart } nextByteOffset = Math.max(nextByteOffset, currentByteStart + byteLength) } const register = normalizeRegister(normalizedSourceRegister, baseGroup, index, address, byteOffset) registers.push(register) if (isBitRegister) nextAddress += register.registerCount } const byteLength = isBitRegisterType(baseGroup.registerType) ? Math.max(1, nextAddress - startAddress) : Math.max(1, nextByteOffset) const paddedByteLength = isBitRegisterType(baseGroup.registerType) ? byteLength : (byteAddressed ? byteLength : alignEvenByteLength(byteLength)) const wordQuantity = isBitRegisterType(baseGroup.registerType) ? Math.max(1, nextAddress - startAddress) : (byteAddressed ? Math.max(1, byteLength) : Math.max(1, paddedByteLength / 2)) const addressSpan = byteAddressed ? Math.max(1, byteLength) : wordQuantity const addressOverflow = isAddressRangeOverflow(startAddress, addressSpan) const endAddress = startAddress + addressSpan - 1 const startAddressText = `0x${padHex(startAddress)}` const endAddressText = addressOverflow ? `0x${padHex(MAX_MODBUS_ADDRESS)}+` : `0x${padHex(endAddress)}` const displayName = isStorageStructGroup(baseGroup) ? stripStorageAreaPrefix(baseGroup.name) : baseGroup.name const listMetaText = isStorageStructGroup(baseGroup) ? `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B` : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}` const detailMetaText = isStorageStructGroup(baseGroup) ? `${getStorageAreaText(baseGroup, true)} ${startAddressText} ${quantity}/${byteLength}B` : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}` const sourceMetaText = createGroupSourceMetaText(baseGroup) return { ...baseGroup, addressRangeText: formatAddressRange(startAddress, addressSpan), addressOverflow, addressWarningText: addressOverflow ? '地址超出 0xFFFF' : '', detailMetaText, detailTitleText: displayName, displayName, endAddressText, functionCode: registerType.functionCode, isReadOnly: !registerType.writable, byteLength, listMetaText, maxQuantity, registerTypeIndex: getRegisterTypeIndex(registerType.key), registerTypeText: registerType.label, registers, paddedByteLength, sourceMetaText, startAddressText, wordQuantity, writable: registerType.writable } } function normalizeGroupConfig(config = {}) { const registerType = config.registerTypeIndex !== undefined && config.registerTypeIndex !== null ? (REGISTER_TYPE_OPTIONS[Number(config.registerTypeIndex)] || REGISTER_TYPE_OPTIONS[0]) : getRegisterType(config.registerType || config.type || DEFAULT_REGISTER_TYPE) const maxQuantity = getMaxQuantity(registerType.key) return { layout: isStructLayout(config.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER, name: String(config.name || config.groupName || '寄存器组').trim() || '寄存器组', quantity: parseConfigQuantity(config.quantity, maxQuantity), registerType: registerType.key, startAddress: parseConfigAddress(config.startAddress) } } function getRegisterJsonValue(register) { if (register.inputValue !== undefined && register.inputValue !== null) { return normalizeTextValue(register.inputValue) } if (register.defaultValue !== undefined && register.defaultValue !== null) { return normalizeTextValue(register.defaultValue) } if (register.displayValue !== undefined && register.displayValue !== null && register.displayValue !== '--') { return normalizeTextValue(register.displayValue) } return '' } function normalizeImportedRegisterDataType(register) { const dataType = register.dataType || register.type || DEFAULT_DATA_TYPE return getDataType(dataType).key } function cloneImportedGroup(group) { return { layout: isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER, name: group.name, quantity: group.quantity, registerType: group.registerType || group.type || DEFAULT_REGISTER_TYPE, registers: (Array.isArray(group.registers) ? group.registers : []).map((register) => ({ conversionFormula: register.conversionFormula, dataType: normalizeImportedRegisterDataType(register), defaultValue: register.defaultValue, inputValue: register.inputValue, maxValue: register.maxValue, minValue: register.minValue, name: register.name, isStructField: !!register.isStructField, textByteLength: register.textByteLength, remark: register.remark, unit: register.unit, value: register.value, ...pickFields(register, STRUCT_REGISTER_FIELDS), ...pickFields(register, SOURCE_REGISTER_FIELDS) })), startAddress: group.startAddress, ...pickFields(group, SOURCE_GROUP_FIELDS) } } module.exports = { DATA_TYPE_OPTIONS, DEFAULT_DATA_TYPE, DEFAULT_REGISTER_TYPE, GROUP_LAYOUT_REGISTER, GROUP_LAYOUT_STRUCT, MAX_MODBUS_ADDRESS, REGISTER_TYPE_OPTIONS, cloneImportedGroup, decodeRegisterFromByteCache, decodeRegisterFromWordCache, decodeRegisterValue, formatCoilDisplayValue, formatRegisterValue, getDataType, getRegisterEncodedBytes, getRegisterEncodedWords, getGroupEncodedBytes, getGroupEncodedWords, getRegisterWordCount, getRegisterJsonValue, getRegisterBytesFromByteCache, getRegisterWordsFromByteCache, getRegisterWordsFromWordCache, getRegisterWriteValueText, isAddressRangeOverflow, isBitRegisterType, isByteRegister, isTextRegister, normalizeGroup, normalizeGroupConfig, normalizeStorageCodeInfoCard, normalizeRegister, parseCoilValue, registerTypeIsBit, splitWordSpans, validateRegisterValue }