const { formatFixedValue } = require('./conversions') const { clampInteger, createId, normalizeTextValue, padHex } = require('./base-utils') const { bytesToWords, getByteFromWord, trimTrailingNullBytes, wordsToBytes } = require('./binary-utils') const MAX_MODBUS_ADDRESS = 0xFFFF const MAX_GENERIC_MODBUS_ITEMS = 256 const DEFAULT_TEXT_BYTE_LENGTH = 32 const MAX_TEXT_BYTE_LENGTH = 32 const REGISTER_TYPE_OPTIONS = [ { functionCode: 0x03, key: 'holding', label: '保持寄存器', writable: true }, { functionCode: 0x01, key: 'coil', label: '线圈', writable: true }, { functionCode: 0x02, key: 'discrete', label: '离散输入状态', writable: false }, { functionCode: 0x04, key: 'input', label: '输入寄存器', writable: false } ] const DATA_TYPE_OPTIONS = [ { byteLength: 1, key: 'int8_t', label: 'int8_t', kind: 'number', wordCount: 1 }, { byteLength: 1, key: 'uint8_t', label: 'uint8_t', kind: 'number', wordCount: 1 }, { byteLength: 2, key: 'int16_t', label: 'int16_t', kind: 'number', wordCount: 1 }, { byteLength: 2, key: 'uint16_t', label: 'uint16_t', kind: 'number', wordCount: 1 }, { byteLength: 4, key: 'int32_t', label: 'int32_t', kind: 'number', wordCount: 2 }, { byteLength: 4, key: 'uint32_t', label: 'uint32_t', kind: 'number', wordCount: 2 }, { byteLength: 4, key: 'float', label: 'float', kind: 'number', wordCount: 2 }, { byteLength: 32, key: 'utf8', label: 'UTF-8', kind: 'text', maxByteLength: MAX_TEXT_BYTE_LENGTH, wordCount: 16 }, { byteLength: 32, key: 'ascii', label: 'ASCII', kind: 'text', maxByteLength: MAX_TEXT_BYTE_LENGTH, wordCount: 16 }, { byteLength: 2, key: 'hex', label: 'HEX', kind: 'hex', wordCount: 1 } ] const DEFAULT_REGISTER_TYPE = REGISTER_TYPE_OPTIONS[0].key const DEFAULT_DATA_TYPE = 'uint16_t' 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 getDataType(dataType) { return DATA_TYPE_OPTIONS.find((item) => item.key === dataType) || DATA_TYPE_OPTIONS.find((item) => item.key === DEFAULT_DATA_TYPE) || DATA_TYPE_OPTIONS[0] } function getDataTypeIndex(dataType) { return Math.max(0, DATA_TYPE_OPTIONS.findIndex((item) => item.key === getDataType(dataType).key)) } function normalizeTextByteLength(value, fallback = DEFAULT_TEXT_BYTE_LENGTH) { const numberValue = Number(value) const rounded = Number.isFinite(numberValue) ? Math.round(numberValue) : fallback return Math.min(Math.max(rounded, 1), MAX_TEXT_BYTE_LENGTH) } function alignEvenByteLength(byteLength) { const length = Math.max(1, Math.round(Number(byteLength) || 1)) return length % 2 === 0 ? length : length + 1 } function getRegisterTextByteLength(register = {}) { return normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH) } function getRegisterByteLength(dataType, register = {}) { const type = getDataType(dataType) if (type.kind === 'text') return alignEvenByteLength(getRegisterTextByteLength(register)) return type.byteLength || ((type.wordCount || 1) * 2) } function getRegisterWordCount(dataType, register = {}) { return Math.max(1, Math.ceil(getRegisterByteLength(dataType, register) / 2)) } function getRegisterWordCountAtOffset(dataType, byteOffset, register = {}) { const byteLength = getRegisterByteLength(dataType, register) return Math.max(1, Math.ceil((byteOffset + byteLength) / 2)) } function getEncodeByteLimit(register) { return isTextRegister(register.dataType) ? getRegisterTextByteLength(register) : getRegisterByteLength(register.dataType, register) } function isTextRegister(dataType) { return getDataType(dataType).kind === 'text' } function isByteRegister(dataType) { const key = getDataType(dataType).key return key === 'int8_t' || key === 'uint8_t' } function isBitRegisterType(registerType) { return registerType === 'coil' || registerType === 'discrete' } function isHexRegister(dataType) { return getDataType(dataType).key === 'hex' } function isNumericRegister(dataType) { return getDataType(dataType).kind === 'number' } function supportsRange(dataType) { return isNumericRegister(dataType) || isHexRegister(dataType) } function supportsUnit(dataType) { return isNumericRegister(dataType) } function padWordHex(value) { return Number(value || 0).toString(16).toUpperCase().padStart(4, '0') } function formatRawWordText(words = []) { if (!Array.isArray(words) || !words.length) return '--' return words.map((word) => `0x${padWordHex(word)}`).join(' ') } 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 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 isAddressRangeOverflow(startAddress, wordCount) { const address = normalizeAddress(startAddress, 0) const count = Math.max(1, Number(wordCount) || 1) return address + count - 1 > MAX_MODBUS_ADDRESS } function encodeAsciiBytes(text, byteLimit = 32) { const bytes = [] const stringValue = normalizeTextValue(text) for (let index = 0; index < stringValue.length; index += 1) { const code = stringValue.charCodeAt(index) if (code > 0x7F) { throw new Error('ASCII 文本只能包含 0x00 - 0x7F 字符') } bytes.push(code) if (bytes.length > byteLimit) break } if (bytes.length > byteLimit) { throw new Error(`长文本最长 ${byteLimit} 字节`) } return bytes } function encodeUtf8Bytes(text, byteLimit = 32) { const bytes = [] const encoded = encodeURIComponent(normalizeTextValue(text)) for (let index = 0; index < encoded.length; index += 1) { const char = encoded[index] if (char === '%') { const byte = parseInt(encoded.slice(index + 1, index + 3), 16) if (!Number.isFinite(byte)) break bytes.push(byte & 0xFF) index += 2 } else { bytes.push(char.charCodeAt(0) & 0xFF) } if (bytes.length > byteLimit) break } if (bytes.length > byteLimit) { throw new Error(`长文本最长 ${byteLimit} 字节`) } return bytes } function decodeAsciiBytes(bytes = []) { return String.fromCharCode.apply(null, trimTrailingNullBytes(bytes).map((byte) => byte & 0xFF)) } function decodeUtf8Bytes(bytes = []) { const trimmed = trimTrailingNullBytes(bytes) if (!trimmed.length) return '' let encoded = '' trimmed.forEach((byte) => { encoded += `%${(byte & 0xFF).toString(16).padStart(2, '0').toUpperCase()}` }) try { return decodeURIComponent(encoded) } catch (error) { return decodeAsciiBytes(trimmed) } } function encodeTextBytes(text, dataType, byteLimit = MAX_TEXT_BYTE_LENGTH) { const normalizedType = getDataType(dataType).key if (normalizedType === 'ascii') return encodeAsciiBytes(text, byteLimit) return encodeUtf8Bytes(text, byteLimit) } function decodeTextBytes(bytes, dataType) { const normalizedType = getDataType(dataType).key return normalizedType === 'ascii' ? decodeAsciiBytes(bytes) : decodeUtf8Bytes(bytes) } function formatIntegerValue(value, dataType) { const type = getDataType(dataType).key const numberValue = Number(value) if (!Number.isFinite(numberValue)) return '--' if (type === 'int8_t') return String(((Math.round(numberValue) << 24) >> 24)) if (type === 'uint8_t') return String(Math.round(numberValue) & 0xFF) if (type === 'int16_t') return String(((Math.round(numberValue) << 16) >> 16)) if (type === 'uint16_t') return String(Math.round(numberValue) & 0xFFFF) if (type === 'int32_t') return String((Math.round(numberValue) | 0)) if (type === 'uint32_t') return String(Math.round(numberValue) >>> 0) return String(Math.round(numberValue)) } function formatHexValue(value) { const numberValue = Number(value) if (!Number.isFinite(numberValue)) return '--' return `0x${padWordHex(Math.round(numberValue) & 0xFFFF)}` } function formatFloatValue(value) { return formatFixedValue(value, 6).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '') } function parseIntegerText(value) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text) return null const isHex = /^[-+]?0x[0-9a-f]+$/i.test(text) || /^0x[0-9a-f]+$/i.test(text) const parsed = isHex ? parseInt(text, 16) : Number(text) return Number.isFinite(parsed) ? parsed : null } function parseHexText(value) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text) return null const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text if (/^[0-9A-F]{1,4}$/i.test(hexText)) { const parsedHex = parseInt(hexText, 16) return Number.isFinite(parsedHex) ? parsedHex : null } return null } function getRegisterValueTypeLabel(dataType) { return getDataType(dataType).label } function getMaxQuantity() { return MAX_GENERIC_MODBUS_ITEMS } function parseCoilValue(value) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text || text === '--') return null if (['1', 'true', 'TRUE', 'on', 'ON', '开'].includes(text)) return 1 if (['0', 'false', 'FALSE', 'off', 'OFF', '关'].includes(text)) return 0 const coilValue = Number(text) return Number.isFinite(coilValue) ? (coilValue ? 1 : 0) : null } function getNumericRange(dataType) { const type = getDataType(dataType).key if (type === 'int8_t') return { max: 127, min: -128 } if (type === 'uint8_t') return { max: 0xFF, min: 0 } if (type === 'int16_t') return { max: 32767, min: -32768 } if (type === 'uint16_t') return { max: 0xFFFF, min: 0 } if (type === 'int32_t') return { max: 2147483647, min: -2147483648 } if (type === 'uint32_t') return { max: 0xFFFFFFFF, min: 0 } if (type === 'hex') return { max: 0xFFFF, min: 0 } return { max: Number.POSITIVE_INFINITY, min: Number.NEGATIVE_INFINITY } } function parseNumberText(value, dataType) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text || text === '--') return null if (getDataType(dataType).key === 'float') { const parsed = Number(text) return Number.isFinite(parsed) ? parsed : null } if (isHexRegister(dataType)) return parseHexText(text) return parseIntegerText(text) } function parseRangeBoundary(value, dataType, label) { const text = String(value === undefined || value === null ? '' : value).trim() if (!text) return null const parsed = parseNumberText(text, dataType) if (parsed === null) { throw new Error(`${label}无效`) } return parsed } function validateNumericValue(register, value) { const dataType = getDataType(register.dataType).key const range = getNumericRange(dataType) const numberValue = Number(value) if (!Number.isFinite(numberValue)) return false if (dataType !== 'float' && Math.round(numberValue) !== numberValue) { throw new Error(`${register.name || '寄存器'} 需要整数`) } if (numberValue < range.min || numberValue > range.max) { throw new Error(`${register.name || '寄存器'} 超出 ${dataType} 范围`) } const minValue = parseRangeBoundary(register.minValue, dataType, `${register.name || '寄存器'} 最小值`) const maxValue = parseRangeBoundary(register.maxValue, dataType, `${register.name || '寄存器'} 最大值`) if (minValue !== null && numberValue < minValue) { throw new Error(`${register.name || '寄存器'} 小于限制最小值`) } if (maxValue !== null && numberValue > maxValue) { throw new Error(`${register.name || '寄存器'} 大于限制最大值`) } return true } function floatToWords(value) { const buffer = new ArrayBuffer(4) const view = new DataView(buffer) view.setFloat32(0, Number(value), false) return [view.getUint16(0, false), view.getUint16(2, false)] } function wordsToFloat(words) { if (!Array.isArray(words) || words.length < 2) return null const buffer = new ArrayBuffer(4) const view = new DataView(buffer) view.setUint16(0, Number(words[0]) & 0xFFFF, false) view.setUint16(2, Number(words[1]) & 0xFFFF, false) return view.getFloat32(0, false) } function encodeRegisterWords(register) { const dataType = getDataType(register.dataType).key const valueText = normalizeTextValue(register.inputValue) if (isTextRegister(dataType)) { const byteLimit = getEncodeByteLimit(register) const byteLength = getRegisterByteLength(dataType, register) const bytes = encodeTextBytes(valueText, dataType, byteLimit) const paddedBytes = bytes.slice() while (paddedBytes.length < byteLength) { paddedBytes.push(0) } return bytesToWords(paddedBytes.slice(0, byteLength)) } const numberValue = parseNumberText(valueText, dataType) if (numberValue === null) return null validateNumericValue(register, numberValue) if (dataType === 'float') return floatToWords(numberValue) const rounded = Math.round(numberValue) if (dataType === 'int8_t') return [((rounded < 0 ? 0x100 + rounded : rounded) & 0xFF)] if (dataType === 'uint8_t') return [rounded & 0xFF] if (dataType === 'int16_t' || dataType === 'uint16_t' || dataType === 'hex') return [rounded & 0xFFFF] const unsignedValue = rounded < 0 ? 0x100000000 + rounded : rounded return [ Math.floor(unsignedValue / 0x10000) & 0xFFFF, unsignedValue & 0xFFFF ] } function decodeRegisterValue(register, words) { const dataType = getDataType(register.dataType).key if (!Array.isArray(words) || words.length < getRegisterWordCount(dataType, register)) return null if (isTextRegister(dataType)) { return decodeTextBytes(wordsToBytes(words, getEncodeByteLimit(register)), dataType) } if (dataType === 'float') { return wordsToFloat(words) } if (dataType === 'int8_t') { const byteValue = getByteFromWord(words[0], register.byteOffset) return byteValue & 0x80 ? byteValue - 0x100 : byteValue } if (dataType === 'uint8_t') { return getByteFromWord(words[0], register.byteOffset) } if (dataType === 'int16_t') { const wordValue = Number(words[0]) & 0xFFFF return wordValue & 0x8000 ? wordValue - 0x10000 : wordValue } if (dataType === 'uint16_t') { return Number(words[0]) & 0xFFFF } if (dataType === 'hex') { return Number(words[0]) & 0xFFFF } const highWord = Number(words[0]) & 0xFFFF const lowWord = Number(words[1]) & 0xFFFF const unsignedValue = highWord * 0x10000 + lowWord if (dataType === 'int32_t') { return unsignedValue >= 0x80000000 ? unsignedValue - 0x100000000 : unsignedValue } return unsignedValue } function formatRegisterValue(register, rawValue) { if (rawValue === null || rawValue === undefined) return '--' const dataType = getDataType(register.dataType).key if (isTextRegister(dataType)) return normalizeTextValue(rawValue) if (dataType === 'hex') return formatHexValue(rawValue) if (dataType === 'float') return formatFloatValue(rawValue) return formatIntegerValue(rawValue, dataType) } function formatCoilDisplayValue(value) { return Number(value) ? '1' : '0' } 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 normalizeRegister(register, group, index, address, byteOffset = 0) { const registerType = getRegisterType(group.registerType).key const dataType = normalizeRegisterDataType(register, registerType) 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, { textByteLength }) const registerCount = isBitRegisterType(registerType) ? 1 : getRegisterWordCountAtOffset(dataType, byteOffset, { textByteLength }) const canShowUnit = !isBitRegisterType(registerType) && supportsUnit(dataType) const rawWords = Array.isArray(register.rawWords) ? register.rawWords.slice(0, registerCount).map((word) => Number(word) & 0xFFFF) : [] const rawValueText = rawValue === null ? '--' : (isBitRegisterType(registerType) ? formatCoilDisplayValue(rawValue) : formatRawWordText(rawWords)) const displayValue = rawValue === null ? (inputValue.trim() ? inputValue : '--') : formatRegisterValue({ ...register, dataType, byteOffset }, rawValue) return { address, addressRangeText: isBitRegisterType(registerType) ? `0x${padHex(address)}` : formatAddressRange(address, registerCount), addressText: formatRegisterAddressText(address, byteOffset, byteLength, registerType), byteLength, byteLengthText: isBitRegisterType(registerType) ? '1bit' : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`), dataType, dataTypeIndex: getDataTypeIndex(dataType), dataTypeText: getRegisterValueTypeLabel(dataType), defaultValue, displayValue, id: register.id || createId('gm-reg'), inputType: isTextRegister(dataType) ? 'text' : 'text', inputValue, isDirty: !!register.isDirty, maxValue: normalizeTextValue(register.maxValue), minValue: normalizeTextValue(register.minValue), name: register.name || `寄存器 ${index + 1}`, rawValue, rawValueText, rawWords, registerCount, byteOffset, registerType, showDataType: !isBitRegisterType(registerType), showRange: !isBitRegisterType(registerType) && supportsRange(dataType), showTextLength: !isBitRegisterType(registerType) && isTextRegister(dataType), showUnit: canShowUnit, textByteLength, unit: canShowUnit ? normalizeTextValue(register.unit).trim() : '', remark: register.remark || '' } } 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 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'), name: String(group.name || group.groupName || '寄存器组').trim() || '寄存器组', quantity, registerType: registerType.key, startAddress, touchStartX: 0 } const registers = [] let nextAddress = startAddress let nextByteOffset = 0 for (let index = 0; index < quantity; index += 1) { const sourceRegister = sourceRegisters[index] || {} 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 byteLength = getRegisterByteLength(dataType, { textByteLength }) if (!isByteRegister(dataType) && nextByteOffset % 2 !== 0) { nextByteOffset += 1 } address = startAddress + Math.floor(nextByteOffset / 2) byteOffset = nextByteOffset % 2 nextByteOffset += byteLength } const register = normalizeRegister(sourceRegister, baseGroup, index, address, byteOffset) registers.push(register) if (isBitRegister) nextAddress += register.registerCount } const wordQuantity = isBitRegisterType(baseGroup.registerType) ? Math.max(1, nextAddress - startAddress) : Math.max(1, Math.ceil(nextByteOffset / 2)) const addressOverflow = isAddressRangeOverflow(startAddress, wordQuantity) const endAddress = startAddress + wordQuantity - 1 return { ...baseGroup, addressRangeText: formatAddressRange(startAddress, wordQuantity), addressOverflow, addressWarningText: addressOverflow ? '地址超出 0xFFFF' : '', endAddressText: addressOverflow ? `0x${padHex(MAX_MODBUS_ADDRESS)}+` : `0x${padHex(endAddress)}`, functionCode: registerType.functionCode, isReadOnly: !registerType.writable, maxQuantity, registerTypeIndex: getRegisterTypeIndex(registerType.key), registerTypeText: registerType.label, registers, startAddressText: `0x${padHex(startAddress)}`, 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 { 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 { name: group.name, quantity: group.quantity, registerType: group.registerType || group.type || DEFAULT_REGISTER_TYPE, registers: (Array.isArray(group.registers) ? group.registers : []).map((register) => ({ dataType: normalizeImportedRegisterDataType(register), defaultValue: register.defaultValue, inputValue: register.inputValue, maxValue: register.maxValue, minValue: register.minValue, name: register.name, textByteLength: register.textByteLength, remark: register.remark, unit: register.unit, value: register.value })), startAddress: group.startAddress } } function splitWordSpans(startAddress, quantity, maxQuantity) { const spans = [] let address = normalizeAddress(startAddress, 0) let remaining = Math.max(0, Math.floor(Number(quantity) || 0)) while (remaining > 0) { const spanQuantity = Math.min(remaining, maxQuantity) spans.push({ address, quantity: spanQuantity }) address += spanQuantity remaining -= spanQuantity } return spans } function getRegisterWriteValueText(register) { if (register.inputValue !== undefined && register.inputValue !== null) return normalizeTextValue(register.inputValue) if (register.defaultValue !== undefined && register.defaultValue !== null) return normalizeTextValue(register.defaultValue) return '' } function getRegisterWordsFromWordCache(register, wordCache) { const words = [] for (let offset = 0; offset < register.registerCount; offset += 1) { const word = wordCache[register.address + offset] if (word === undefined) return null words.push(word) } return words } function decodeRegisterFromWordCache(register, wordCache) { const words = getRegisterWordsFromWordCache(register, wordCache) if (!words) return null return decodeRegisterValue(register, words) } function getRegisterEncodedWords(register) { return encodeRegisterWords({ ...register, inputValue: getRegisterWriteValueText(register) }) } function validateRegisterValue(register, value) { const valueText = normalizeTextValue( value === undefined || value === null ? getRegisterWriteValueText(register) : value ).trim() if (!valueText || valueText === '--') return true if (registerTypeIsBit(register)) { if (parseCoilValue(valueText) === null) { throw new Error(`${register.name || '线圈'} 只能填写 0 或 1`) } return true } const dataType = getDataType(register.dataType).key if (isTextRegister(dataType)) { encodeTextBytes(valueText, dataType, getEncodeByteLimit(register)) return true } const numberValue = parseNumberText(valueText, dataType) if (numberValue === null) { throw new Error(`${register.name || '寄存器'} 输入值无效`) } return validateNumericValue(register, numberValue) } function registerTypeIsBit(register) { return !!register && isBitRegisterType(register.registerType) } module.exports = { DATA_TYPE_OPTIONS, DEFAULT_DATA_TYPE, DEFAULT_REGISTER_TYPE, MAX_MODBUS_ADDRESS, REGISTER_TYPE_OPTIONS, cloneImportedGroup, decodeRegisterFromWordCache, decodeRegisterValue, formatCoilDisplayValue, formatRegisterValue, getDataType, getRegisterEncodedWords, getRegisterJsonValue, getRegisterWordsFromWordCache, getRegisterWriteValueText, isAddressRangeOverflow, isBitRegisterType, isByteRegister, normalizeGroup, normalizeGroupConfig, parseCoilValue, registerTypeIsBit, splitWordSpans, validateRegisterValue }