| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923 |
- 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
- }
|