|
|
@@ -1,1398 +0,0 @@
|
|
|
-const {
|
|
|
- formatFixedValue
|
|
|
-} = require('../../utils/number-format.js')
|
|
|
-const {
|
|
|
- clampInteger,
|
|
|
- createId,
|
|
|
- normalizeTextValue,
|
|
|
- padHex
|
|
|
-} = require('../../utils/base-utils.js')
|
|
|
-const {
|
|
|
- bytesToWords,
|
|
|
- getByteFromWord,
|
|
|
- trimTrailingNullBytes,
|
|
|
- wordsToBytes
|
|
|
-} = require('../../utils/binary-utils.js')
|
|
|
-
|
|
|
-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'
|
|
|
-const GROUP_LAYOUT_REGISTER = 'register'
|
|
|
-const GROUP_LAYOUT_STRUCT = 'struct'
|
|
|
-const BYTE_ADDRESS_MEMORY_AREAS = ['BIT', 'CODE', 'DATA', 'IDATA', 'XDATA']
|
|
|
-const SOURCE_REGISTER_FIELDS = [
|
|
|
- 'sourceAddress',
|
|
|
- 'sourceAddressText',
|
|
|
- 'sourceByteLength',
|
|
|
- 'sourceBitOffset',
|
|
|
- 'sourceBitWidth',
|
|
|
- 'sourceMemoryArea',
|
|
|
- 'sourceMemoryClass',
|
|
|
- 'sourceSymbolName',
|
|
|
- 'sourceSymbolType'
|
|
|
-]
|
|
|
-const STRUCT_REGISTER_FIELDS = [
|
|
|
- 'bitOffset',
|
|
|
- 'bitWidth',
|
|
|
- 'byteStart',
|
|
|
- 'isBitField',
|
|
|
- 'structByteLength'
|
|
|
-]
|
|
|
-const SOURCE_GROUP_FIELDS = [
|
|
|
- 'addressUnit',
|
|
|
- 'sourceAddress',
|
|
|
- 'sourceAddressText',
|
|
|
- 'sourceByteLength',
|
|
|
- 'sourceMemoryArea',
|
|
|
- 'sourceMemoryClass',
|
|
|
- 'sourceSegment',
|
|
|
- 'sourceSegmentModule',
|
|
|
- 'sourceSymbolName'
|
|
|
-]
|
|
|
-
|
|
|
-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 isStructLayout(layout) {
|
|
|
- return layout === GROUP_LAYOUT_STRUCT
|
|
|
-}
|
|
|
-
|
|
|
-function isBitFieldRegister(register = {}) {
|
|
|
- return !!register.isBitField
|
|
|
-}
|
|
|
-
|
|
|
-function normalizeBitOffset(value) {
|
|
|
- const numberValue = Math.floor(Number(value) || 0)
|
|
|
-
|
|
|
- return Math.min(Math.max(numberValue, 0), 7)
|
|
|
-}
|
|
|
-
|
|
|
-function normalizeBitWidth(value) {
|
|
|
- const numberValue = Math.round(Number(value) || 1)
|
|
|
-
|
|
|
- return Math.min(Math.max(numberValue, 1), 32)
|
|
|
-}
|
|
|
-
|
|
|
-function getBitFieldByteLength(register = {}) {
|
|
|
- const bitOffset = normalizeBitOffset(register.bitOffset)
|
|
|
- const bitWidth = normalizeBitWidth(register.bitWidth)
|
|
|
-
|
|
|
- return Math.max(1, Math.ceil((bitOffset + bitWidth) / 8))
|
|
|
-}
|
|
|
-
|
|
|
-function getBitFieldMaxValue(register = {}) {
|
|
|
- const bitWidth = normalizeBitWidth(register.bitWidth)
|
|
|
-
|
|
|
- return bitWidth >= 32 ? 0xFFFFFFFF : Math.pow(2, bitWidth) - 1
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterByteLength(dataType, register = {}) {
|
|
|
- if (isBitFieldRegister(register)) return getBitFieldByteLength(register)
|
|
|
-
|
|
|
- const type = getDataType(dataType)
|
|
|
- if (type.kind === 'text') {
|
|
|
- const byteLength = getRegisterTextByteLength(register)
|
|
|
-
|
|
|
- return isStructLayout(register.layout) ? byteLength : alignEvenByteLength(byteLength)
|
|
|
- }
|
|
|
-
|
|
|
- return type.byteLength || ((type.wordCount || 1) * 2)
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterWordCount(dataType, register = {}) {
|
|
|
- return Math.max(1, Math.ceil(getRegisterByteLength(dataType, register) / 2))
|
|
|
-}
|
|
|
-
|
|
|
-function getByteSpanWordCount(byteOffset, byteLength) {
|
|
|
- return Math.max(1, Math.ceil((Math.max(0, Number(byteOffset) || 0) + Math.max(1, Number(byteLength) || 1)) / 2))
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterWordCountAtOffset(dataType, byteOffset, register = {}) {
|
|
|
- const byteLength = getRegisterByteLength(dataType, register)
|
|
|
- return getByteSpanWordCount(byteOffset, byteLength)
|
|
|
-}
|
|
|
-
|
|
|
-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 formatRawByteText(bytes = []) {
|
|
|
- if (!Array.isArray(bytes) || !bytes.length) return '--'
|
|
|
-
|
|
|
- return bytes.map((byte) => `0x${(Number(byte) & 0xFF).toString(16).toUpperCase().padStart(2, '0')}`).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 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 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 floatToBytes(value) {
|
|
|
- const buffer = new ArrayBuffer(4)
|
|
|
- const view = new DataView(buffer)
|
|
|
-
|
|
|
- view.setFloat32(0, Number(value), false)
|
|
|
-
|
|
|
- return [
|
|
|
- view.getUint8(0),
|
|
|
- view.getUint8(1),
|
|
|
- view.getUint8(2),
|
|
|
- view.getUint8(3)
|
|
|
- ]
|
|
|
-}
|
|
|
-
|
|
|
-function bytesToFloatValue(bytes) {
|
|
|
- if (!Array.isArray(bytes) || bytes.length < 4) return null
|
|
|
-
|
|
|
- const buffer = new ArrayBuffer(4)
|
|
|
- const view = new DataView(buffer)
|
|
|
-
|
|
|
- for (let index = 0; index < 4; index += 1) {
|
|
|
- view.setUint8(index, Number(bytes[index]) & 0xFF)
|
|
|
- }
|
|
|
-
|
|
|
- return view.getFloat32(0, false)
|
|
|
-}
|
|
|
-
|
|
|
-function unsignedIntegerToBytes(value, byteLength) {
|
|
|
- let numberValue = Math.round(Number(value) || 0)
|
|
|
- const bytes = []
|
|
|
-
|
|
|
- if (numberValue < 0) {
|
|
|
- numberValue += Math.pow(2, byteLength * 8)
|
|
|
- }
|
|
|
-
|
|
|
- for (let index = byteLength - 1; index >= 0; index -= 1) {
|
|
|
- bytes[index] = numberValue & 0xFF
|
|
|
- numberValue = Math.floor(numberValue / 0x100)
|
|
|
- }
|
|
|
-
|
|
|
- return bytes
|
|
|
-}
|
|
|
-
|
|
|
-function bytesToUnsignedInteger(bytes) {
|
|
|
- return bytes.reduce((value, byte) => ((value * 0x100) + (Number(byte) & 0xFF)), 0)
|
|
|
-}
|
|
|
-
|
|
|
-function bytesToSignedInteger(bytes) {
|
|
|
- const unsignedValue = bytesToUnsignedInteger(bytes)
|
|
|
- const signLimit = Math.pow(2, bytes.length * 8 - 1)
|
|
|
- const fullRange = Math.pow(2, bytes.length * 8)
|
|
|
-
|
|
|
- return unsignedValue >= signLimit ? unsignedValue - fullRange : unsignedValue
|
|
|
-}
|
|
|
-
|
|
|
-function parseBitFieldValue(register, valueText) {
|
|
|
- const text = normalizeTextValue(valueText).trim()
|
|
|
- const bitWidth = normalizeBitWidth(register.bitWidth)
|
|
|
- let parsed = parseNumberText(text, 'uint32_t')
|
|
|
- const maxValue = getBitFieldMaxValue(register)
|
|
|
-
|
|
|
- if (parsed === null && bitWidth === 1) parsed = parseCoilValue(text)
|
|
|
- if (parsed === null) return null
|
|
|
- if (Math.round(parsed) !== parsed || parsed < 0 || parsed > maxValue) {
|
|
|
- throw new Error(`${register.name || '位域'} 超出 0 - ${maxValue} 范围`)
|
|
|
- }
|
|
|
-
|
|
|
- return Math.round(parsed)
|
|
|
-}
|
|
|
-
|
|
|
-function decodeBitFieldBytes(register, bytes = []) {
|
|
|
- const bitOffset = normalizeBitOffset(register.bitOffset)
|
|
|
- const bitWidth = normalizeBitWidth(register.bitWidth)
|
|
|
- let byteIndex = 0
|
|
|
- let currentBitOffset = bitOffset
|
|
|
- let multiplier = 1
|
|
|
- let remaining = bitWidth
|
|
|
- let value = 0
|
|
|
-
|
|
|
- while (remaining > 0 && byteIndex < bytes.length) {
|
|
|
- const take = Math.min(8 - currentBitOffset, remaining)
|
|
|
- const mask = (1 << take) - 1
|
|
|
- const part = ((Number(bytes[byteIndex]) & 0xFF) >> currentBitOffset) & mask
|
|
|
-
|
|
|
- value += part * multiplier
|
|
|
- multiplier *= Math.pow(2, take)
|
|
|
- remaining -= take
|
|
|
- byteIndex += 1
|
|
|
- currentBitOffset = 0
|
|
|
- }
|
|
|
-
|
|
|
- return remaining > 0 ? null : value
|
|
|
-}
|
|
|
-
|
|
|
-function encodeBitFieldIntoBytes(register, bytes, byteStart = 0) {
|
|
|
- const valueText = normalizeTextValue(register.inputValue)
|
|
|
- let value = parseBitFieldValue(register, valueText)
|
|
|
- const bitOffset = normalizeBitOffset(register.bitOffset)
|
|
|
- const bitWidth = normalizeBitWidth(register.bitWidth)
|
|
|
- let byteIndex = Math.max(0, Math.floor(Number(byteStart) || 0))
|
|
|
- let currentBitOffset = bitOffset
|
|
|
- let remaining = bitWidth
|
|
|
-
|
|
|
- if (value === null) return null
|
|
|
-
|
|
|
- while (remaining > 0) {
|
|
|
- const take = Math.min(8 - currentBitOffset, remaining)
|
|
|
- const mask = (1 << take) - 1
|
|
|
- const shiftedMask = (mask << currentBitOffset) & 0xFF
|
|
|
- const part = value & mask
|
|
|
-
|
|
|
- if (byteIndex >= bytes.length) return null
|
|
|
- bytes[byteIndex] = ((Number(bytes[byteIndex]) & 0xFF) & (~shiftedMask & 0xFF))
|
|
|
- | ((part << currentBitOffset) & shiftedMask)
|
|
|
-
|
|
|
- value = Math.floor(value / Math.pow(2, take))
|
|
|
- remaining -= take
|
|
|
- byteIndex += 1
|
|
|
- currentBitOffset = 0
|
|
|
- }
|
|
|
-
|
|
|
- return bytes
|
|
|
-}
|
|
|
-
|
|
|
-function encodeBitFieldBytes(register) {
|
|
|
- const bytes = Array.from({ length: getBitFieldByteLength(register) }, () => 0)
|
|
|
-
|
|
|
- return encodeBitFieldIntoBytes(register, bytes, 0)
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterDataBytes(register, words) {
|
|
|
- const dataType = getDataType(register.dataType).key
|
|
|
- const byteLength = getRegisterByteLength(dataType, register)
|
|
|
- const byteOffset = Math.max(0, Math.floor(Number(register.byteOffset) || 0))
|
|
|
- const sourceBytes = wordsToBytes(words, Math.max(0, (Array.isArray(words) ? words.length : 0) * 2))
|
|
|
-
|
|
|
- return sourceBytes.slice(byteOffset, byteOffset + byteLength)
|
|
|
-}
|
|
|
-
|
|
|
-function encodeRegisterBytes(register) {
|
|
|
- const dataType = getDataType(register.dataType).key
|
|
|
- const valueText = normalizeTextValue(register.inputValue)
|
|
|
- const byteLength = getRegisterByteLength(dataType, register)
|
|
|
-
|
|
|
- if (isBitFieldRegister(register)) {
|
|
|
- return encodeBitFieldBytes(register)
|
|
|
- }
|
|
|
-
|
|
|
- if (isTextRegister(dataType)) {
|
|
|
- const byteLimit = getEncodeByteLimit(register)
|
|
|
- const bytes = encodeTextBytes(valueText, dataType, byteLimit)
|
|
|
- const paddedBytes = bytes.slice()
|
|
|
-
|
|
|
- while (paddedBytes.length < byteLength) {
|
|
|
- paddedBytes.push(0)
|
|
|
- }
|
|
|
-
|
|
|
- return paddedBytes.slice(0, byteLength)
|
|
|
- }
|
|
|
-
|
|
|
- const numberValue = parseNumberText(valueText, dataType)
|
|
|
- if (numberValue === null) return null
|
|
|
- validateNumericValue(register, numberValue)
|
|
|
-
|
|
|
- if (dataType === 'float') return floatToBytes(numberValue)
|
|
|
-
|
|
|
- const rounded = Math.round(numberValue)
|
|
|
- if (dataType === 'int8_t' || dataType === 'uint8_t') return [rounded & 0xFF]
|
|
|
- if (dataType === 'int16_t' || dataType === 'uint16_t' || dataType === 'hex') {
|
|
|
- return unsignedIntegerToBytes(rounded, 2)
|
|
|
- }
|
|
|
- if (dataType === 'int32_t' || dataType === 'uint32_t') {
|
|
|
- return unsignedIntegerToBytes(rounded, 4)
|
|
|
- }
|
|
|
-
|
|
|
- return unsignedIntegerToBytes(rounded, byteLength)
|
|
|
-}
|
|
|
-
|
|
|
-function encodeRegisterWords(register) {
|
|
|
- const dataType = getDataType(register.dataType).key
|
|
|
- const bytes = encodeRegisterBytes(register)
|
|
|
-
|
|
|
- if (!Array.isArray(bytes)) return null
|
|
|
- if (isByteRegister(dataType)) return [bytes[0] & 0xFF]
|
|
|
-
|
|
|
- return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
|
|
|
-}
|
|
|
-
|
|
|
-function decodeRegisterValue(register, words) {
|
|
|
- const dataType = getDataType(register.dataType).key
|
|
|
-
|
|
|
- if (!Array.isArray(words) || words.length < getRegisterWordCountAtOffset(dataType, register.byteOffset || 0, register)) return null
|
|
|
-
|
|
|
- const bytes = getRegisterDataBytes(register, words)
|
|
|
- const byteLength = getRegisterByteLength(dataType, register)
|
|
|
- if (bytes.length < byteLength) return null
|
|
|
-
|
|
|
- if (isBitFieldRegister(register)) {
|
|
|
- return decodeBitFieldBytes(register, bytes)
|
|
|
- }
|
|
|
-
|
|
|
- if (isTextRegister(dataType)) {
|
|
|
- return decodeTextBytes(bytes.slice(0, getEncodeByteLimit(register)), dataType)
|
|
|
- }
|
|
|
- if (dataType === 'float') {
|
|
|
- return bytesToFloatValue(bytes)
|
|
|
- }
|
|
|
- if (dataType === 'int8_t') {
|
|
|
- const byteValue = bytes[0] & 0xFF
|
|
|
- return byteValue & 0x80 ? byteValue - 0x100 : byteValue
|
|
|
- }
|
|
|
- if (dataType === 'uint8_t') {
|
|
|
- return bytes[0] & 0xFF
|
|
|
- }
|
|
|
- if (dataType === 'int16_t') {
|
|
|
- return bytesToSignedInteger(bytes.slice(0, 2))
|
|
|
- }
|
|
|
- if (dataType === 'uint16_t') {
|
|
|
- return bytesToUnsignedInteger(bytes.slice(0, 2))
|
|
|
- }
|
|
|
- if (dataType === 'hex') {
|
|
|
- return bytesToUnsignedInteger(bytes.slice(0, 2))
|
|
|
- }
|
|
|
-
|
|
|
- if (dataType === 'int32_t') {
|
|
|
- return bytesToSignedInteger(bytes.slice(0, 4))
|
|
|
- }
|
|
|
-
|
|
|
- return bytesToUnsignedInteger(bytes.slice(0, 4))
|
|
|
-}
|
|
|
-
|
|
|
-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 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 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 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 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 rawValueText = rawValue === null
|
|
|
- ? '--'
|
|
|
- : (isBitRegisterType(registerType)
|
|
|
- ? formatCoilDisplayValue(rawValue)
|
|
|
- : (byteAddressed ? formatRawByteText(rawBytes) : formatRawWordText(rawWords)))
|
|
|
- const displayValue = rawValue === null
|
|
|
- ? (inputValue.trim() ? inputValue : '--')
|
|
|
- : formatRegisterValue({ ...register, dataType, byteOffset }, rawValue)
|
|
|
-
|
|
|
- return {
|
|
|
- address,
|
|
|
- addressRangeText: isBitRegisterType(registerType)
|
|
|
- ? `0x${padHex(address)}`
|
|
|
- : (byteAddressed
|
|
|
- ? formatAddressRange(address, Math.max(1, byteLength))
|
|
|
- : formatAddressRange(address, registerCount)),
|
|
|
- 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)),
|
|
|
- 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,
|
|
|
- displayValue,
|
|
|
- id: register.id || createId('gm-reg'),
|
|
|
- inputType: isTextRegister(dataType) ? 'text' : 'text',
|
|
|
- inputValue,
|
|
|
- isStructField: layout === GROUP_LAYOUT_STRUCT || !!register.isStructField,
|
|
|
- layout,
|
|
|
- isBitField,
|
|
|
- isDirty: !!register.isDirty,
|
|
|
- maxValue: normalizeTextValue(register.maxValue),
|
|
|
- minValue: normalizeTextValue(register.minValue),
|
|
|
- name: register.name || `寄存器 ${index + 1}`,
|
|
|
- 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 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,
|
|
|
- ...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
|
|
|
-
|
|
|
- return {
|
|
|
- ...baseGroup,
|
|
|
- addressRangeText: formatAddressRange(startAddress, addressSpan),
|
|
|
- addressOverflow,
|
|
|
- addressWarningText: addressOverflow ? '地址超出 0xFFFF' : '',
|
|
|
- endAddressText: addressOverflow ? `0x${padHex(MAX_MODBUS_ADDRESS)}+` : `0x${padHex(endAddress)}`,
|
|
|
- functionCode: registerType.functionCode,
|
|
|
- isReadOnly: !registerType.writable,
|
|
|
- byteLength,
|
|
|
- maxQuantity,
|
|
|
- registerTypeIndex: getRegisterTypeIndex(registerType.key),
|
|
|
- registerTypeText: registerType.label,
|
|
|
- registers,
|
|
|
- paddedByteLength,
|
|
|
- sourceMetaText: createGroupSourceMetaText(baseGroup),
|
|
|
- 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 {
|
|
|
- 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) => ({
|
|
|
- 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)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-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 getRegisterBytesFromByteCache(register, byteCache) {
|
|
|
- const bytes = []
|
|
|
- for (let offset = 0; offset < register.byteLength; offset += 1) {
|
|
|
- const byte = byteCache[register.address + offset]
|
|
|
- if (byte === undefined) return null
|
|
|
- bytes.push(Number(byte) & 0xFF)
|
|
|
- }
|
|
|
-
|
|
|
- return bytes
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterWordsFromByteCache(register, byteCache) {
|
|
|
- const bytes = getRegisterBytesFromByteCache(register, byteCache)
|
|
|
- if (!bytes) return null
|
|
|
-
|
|
|
- return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
|
|
|
-}
|
|
|
-
|
|
|
-function decodeRegisterFromBytes(register, bytes) {
|
|
|
- if (!Array.isArray(bytes) || bytes.length < register.byteLength) return null
|
|
|
-
|
|
|
- return decodeRegisterValue(
|
|
|
- {
|
|
|
- ...register,
|
|
|
- byteOffset: 0
|
|
|
- },
|
|
|
- bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function decodeRegisterFromWordCache(register, wordCache) {
|
|
|
- const words = getRegisterWordsFromWordCache(register, wordCache)
|
|
|
- if (!words) return null
|
|
|
-
|
|
|
- return decodeRegisterValue(register, words)
|
|
|
-}
|
|
|
-
|
|
|
-function decodeRegisterFromByteCache(register, byteCache) {
|
|
|
- const bytes = getRegisterBytesFromByteCache(register, byteCache)
|
|
|
- if (!bytes) return null
|
|
|
-
|
|
|
- return decodeRegisterFromBytes(register, bytes)
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterEncodedWords(register) {
|
|
|
- return encodeRegisterWords({
|
|
|
- ...register,
|
|
|
- inputValue: getRegisterWriteValueText(register)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterEncodedBytes(register) {
|
|
|
- return encodeRegisterBytes({
|
|
|
- ...register,
|
|
|
- inputValue: getRegisterWriteValueText(register)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function getRegisterByteStart(register, groupStartAddress = 0) {
|
|
|
- if (Number.isFinite(Number(register.byteStart))) {
|
|
|
- return Math.max(0, Math.floor(Number(register.byteStart)))
|
|
|
- }
|
|
|
-
|
|
|
- return Math.max(0, ((Number(register.address) || 0) - (Number(groupStartAddress) || 0)) * 2 + (Number(register.byteOffset) || 0))
|
|
|
-}
|
|
|
-
|
|
|
-function getGroupEncodedBytes(group, baseBytes = null) {
|
|
|
- const byteLength = Math.max(2, Number(group && group.paddedByteLength) || ((Number(group && group.wordQuantity) || 1) * 2))
|
|
|
- const bytes = Array.isArray(baseBytes)
|
|
|
- ? baseBytes.slice(0, byteLength).map((byte) => Number(byte) & 0xFF)
|
|
|
- : []
|
|
|
- const registers = Array.isArray(group && group.registers) ? group.registers : []
|
|
|
-
|
|
|
- while (bytes.length < byteLength) {
|
|
|
- bytes.push(0)
|
|
|
- }
|
|
|
-
|
|
|
- registers.forEach((register) => {
|
|
|
- const byteStart = getRegisterByteStart(register, group.startAddress)
|
|
|
-
|
|
|
- if (isBitFieldRegister(register)) {
|
|
|
- const encodedBytes = encodeBitFieldIntoBytes(
|
|
|
- {
|
|
|
- ...register,
|
|
|
- inputValue: getRegisterWriteValueText(register)
|
|
|
- },
|
|
|
- bytes,
|
|
|
- byteStart
|
|
|
- )
|
|
|
-
|
|
|
- if (!Array.isArray(encodedBytes)) {
|
|
|
- throw new Error(`${register.name || '位域'} 没有有效写入值`)
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const registerBytes = encodeRegisterBytes(register)
|
|
|
- if (!Array.isArray(registerBytes) || !registerBytes.length) {
|
|
|
- throw new Error(`${register.name || '寄存器'} 没有有效写入值`)
|
|
|
- }
|
|
|
-
|
|
|
- for (let offset = 0; offset < register.byteLength; offset += 1) {
|
|
|
- if (byteStart + offset < bytes.length) {
|
|
|
- bytes[byteStart + offset] = Number(registerBytes[offset] || 0) & 0xFF
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- return bytes
|
|
|
-}
|
|
|
-
|
|
|
-function getGroupEncodedWords(group, baseBytes = null) {
|
|
|
- return bytesToWords(getGroupEncodedBytes(group, baseBytes))
|
|
|
-}
|
|
|
-
|
|
|
-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
|
|
|
- }
|
|
|
-
|
|
|
- if (isBitFieldRegister(register)) {
|
|
|
- if (parseBitFieldValue(register, valueText) === null) {
|
|
|
- throw new Error(`${register.name || '位域'} 输入值无效`)
|
|
|
- }
|
|
|
- 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,
|
|
|
- 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,
|
|
|
- normalizeRegister,
|
|
|
- parseCoilValue,
|
|
|
- registerTypeIsBit,
|
|
|
- splitWordSpans,
|
|
|
- validateRegisterValue
|
|
|
-}
|