Browse Source

tlv完善,ui调整

avery 4 ngày trước cách đây
mục cha
commit
a8d5132638

+ 21 - 0
app.wxss

@@ -305,6 +305,7 @@ page {
 
 .theme-dark .generic-readonly-value,
 .theme-dark .generic-register-unit,
+.theme-dark .generic-scalar-value,
 .theme-dark .storage-code-info-model {
   color: #5eead4;
 }
@@ -1305,6 +1306,26 @@ page {
   transform: rotate(-45deg);
 }
 
+.generic-scalar-heading {
+  min-width: 0;
+  flex: 1;
+}
+
+.generic-scalar-value {
+  flex: none;
+  min-width: 96rpx;
+  max-width: 220rpx;
+  color: #0f8f87;
+  font-family: Menlo, Monaco, Consolas, monospace;
+  font-size: 25rpx;
+  line-height: 1.25;
+  font-weight: 900;
+  text-align: right;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
 .generic-group-inline-registers {
   border-top: 1rpx solid #edf2f7;
 }

+ 67 - 10
domain/parameter-groups/constants.js

@@ -2,7 +2,7 @@ const MAX_MODBUS_ADDRESS = 0xFFFF
 const MAX_STORAGE_ADDRESS = 0xFFFFFFFF
 const MAX_PARAMETER_GROUP_ITEMS = 256
 const DEFAULT_TEXT_BYTE_LENGTH = 32
-const MAX_TEXT_BYTE_LENGTH = 32
+const MAX_TEXT_BYTE_LENGTH = 0xFFFF
 
 const REGISTER_TYPE_OPTIONS = [
   {
@@ -81,21 +81,62 @@ const DATA_TYPE_OPTIONS = [
     kind: 'number',
     wordCount: 2
   },
+  {
+    byteLength: 8,
+    key: 'double',
+    label: 'double',
+    kind: 'number',
+    wordCount: 4
+  },
+  {
+    byteLength: 8,
+    key: 'int64_t',
+    label: 'int64_t',
+    kind: 'number',
+    wordCount: 4
+  },
+  {
+    byteLength: 8,
+    key: 'uint64_t',
+    label: 'uint64_t',
+    kind: 'number',
+    wordCount: 4
+  },
+  {
+    byteLength: 16,
+    key: 'int128_t',
+    label: 'int128_t',
+    kind: 'number',
+    wordCount: 8
+  },
+  {
+    byteLength: 16,
+    key: 'uint128_t',
+    label: 'uint128_t',
+    kind: 'number',
+    wordCount: 8
+  },
   {
     byteLength: 32,
-    key: 'utf8',
-    label: 'UTF-8',
-    kind: 'text',
-    maxByteLength: MAX_TEXT_BYTE_LENGTH,
+    key: 'int256_t',
+    label: 'int256_t',
+    kind: 'number',
     wordCount: 16
   },
   {
     byteLength: 32,
-    key: 'ascii',
-    label: 'ASCII',
+    key: 'uint256_t',
+    label: 'uint256_t',
+    kind: 'number',
+    wordCount: 16
+  },
+  {
+    byteLength: DEFAULT_TEXT_BYTE_LENGTH,
+    key: 'text',
+    label: '文本',
     kind: 'text',
     maxByteLength: MAX_TEXT_BYTE_LENGTH,
-    wordCount: 16
+    wordCount: DEFAULT_TEXT_BYTE_LENGTH / 2
   },
   {
     byteLength: 2,
@@ -127,14 +168,23 @@ const SOURCE_REGISTER_FIELDS = [
   'sourceAddressByteLength',
   'sourceAddressText',
   'sourceAddressWidth',
+  'sourceArrayDimensions',
+  'sourceArrayIndex',
+  'sourceArrayIndexPath',
   'sourceByteLength',
+  'sourceDefinitionName',
+  'sourceElementByteLength',
+  'sourceElementCount',
+  'sourceElementType',
   'sourceBitOffset',
   'sourceBitWidth',
   'sourceEntryKind',
+  'sourceInstanceName',
   'sourceMemoryArea',
   'sourceMemoryClass',
   'sourceSymbolName',
-  'sourceSymbolType'
+  'sourceSymbolType',
+  'sourceValueType'
 ]
 
 const STRUCT_REGISTER_FIELDS = [
@@ -153,14 +203,21 @@ const SOURCE_GROUP_FIELDS = [
   'sourceAddressByteLength',
   'sourceAddressText',
   'sourceAddressWidth',
+  'sourceArrayDimensions',
   'sourceByteLength',
+  'sourceDefinitionName',
+  'sourceElementByteLength',
+  'sourceElementCount',
+  'sourceElementType',
   'sourceEntryKind',
+  'sourceInstanceName',
   'sourceMemoryArea',
   'sourceMemoryClass',
   'sourceSegment',
   'sourceSegmentModule',
   'sourceSymbolName',
-  'sourceSymbolType'
+  'sourceSymbolType',
+  'sourceValueType'
 ]
 
 module.exports = {

+ 71 - 3
domain/parameter-groups/model.js

@@ -343,7 +343,7 @@ function assignOptionalNumber(target, key, value) {
   target[key] = value
 }
 
-function normalizeMemoryEndian(value, fallback = 'little') {
+function normalizeMemoryEndian(value, fallback = 'big') {
   const text = String(value || '').trim().toLowerCase()
   if (text === 'little' || text === 'le' || text === '1') return 'little'
   if (text === 'big' || text === 'be' || text === '0') return 'big'
@@ -362,7 +362,7 @@ function normalizeStorageCodeInfoCard(codeInfo = null) {
     caveFreq: hasCodeInfo ? readOptionalNumber(codeInfo.caveFreq) : null,
     chipModel: hasCodeInfo ? String(codeInfo.chipModel || '').trim() : '',
     maxPacketLength: hasCodeInfo ? (Number(codeInfo.maxPacketLength) || 0) : 0,
-    memoryEndian: hasCodeInfo ? normalizeMemoryEndian(codeInfo.memoryEndian) : 'little',
+    memoryEndian: hasCodeInfo ? normalizeMemoryEndian(codeInfo.memoryEndian) : 'big',
     memoryEndianRaw: hasCodeInfo ? (Number(codeInfo.memoryEndianRaw) || 0) : 0,
     model: hasCodeInfo ? String(codeInfo.model || '').trim() : '',
     refVolt: hasCodeInfo
@@ -417,6 +417,18 @@ function isStorageMemoryGroup(group = {}) {
     && !!String(group.sourceMemoryArea || '').trim()
 }
 
+function isStorageScalarEntryKind(entryKind) {
+  const text = String(entryKind || '').trim().toLowerCase()
+
+  return text === 'enum' || text === 'variable'
+}
+
+function isStorageScalarGroup(group = {}) {
+  return isStorageMemoryGroup(group)
+    && !isStructLayout(group.layout)
+    && isStorageScalarEntryKind(group.sourceEntryKind)
+}
+
 function isParameterGroupPollEnabled(group = {}) {
   return group.pollEnabled !== false
 }
@@ -462,6 +474,44 @@ function formatStorageRegisterHexValue(register, rawBytes = []) {
   return formatStorageHexBytes(rawBytes, register.byteLength)
 }
 
+function findEnumOptionByValue(register = {}) {
+  const rawValue = Number(register.rawValue)
+  if (!Number.isFinite(rawValue)) return null
+
+  return (Array.isArray(register.enumOptions) ? register.enumOptions : []).find((option) => (
+    Number(option && option.value) === rawValue
+  )) || null
+}
+
+function formatScalarDecimalValue(register = {}) {
+  const rawValue = register.rawValue
+  if (rawValue === null || rawValue === undefined || Array.isArray(rawValue)) return '--'
+
+  const dataType = getDataType(register.dataType).key
+  if (/^(?:u?int)(?:64|128|256)_t$/.test(dataType)) {
+    return normalizeTextValue(rawValue || register.displayValue || '--') || '--'
+  }
+
+  const numberValue = Number(rawValue)
+  if (!Number.isFinite(numberValue)) return normalizeTextValue(register.displayValue || '--') || '--'
+
+  return (dataType === 'float' || dataType === 'double')
+    ? formatCompactNumber(numberValue, 6)
+    : String(Math.trunc(numberValue))
+}
+
+function createScalarDefinitionText(register = {}, group = {}) {
+  const enumOption = findEnumOptionByValue(register)
+  const typeName = String(register.enumName || register.sourceSymbolType || group.sourceSymbolType || '').trim()
+  const dataTypeText = register.dataType === 'raw' ? '' : (register.dataTypeText || register.dataType)
+  const definitionText = typeName || dataTypeText
+
+  if (enumOption && definitionText) return `${definitionText}:${enumOption.label || enumOption.name}`
+  if (enumOption) return enumOption.label || enumOption.name
+
+  return definitionText
+}
+
 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
@@ -695,6 +745,7 @@ function normalizeGroup(group) {
     : (byteAddressed ? Math.max(1, byteLength) : Math.max(1, paddedByteLength / 2))
   const addressSpan = byteAddressed ? Math.max(1, byteLength) : wordQuantity
   const storageMemory = isStorageMemoryGroup(baseGroup)
+  const storageScalarGroup = isStorageScalarGroup(baseGroup)
   const storageAddressWidth = storageMemory ? resolveStorageAddressWidth(baseGroup, startAddress) : 0
   const storageAddressMax = getStorageAddressMax(storageAddressWidth, startAddress)
   const addressOverflow = storageMemory
@@ -708,11 +759,23 @@ function normalizeGroup(group) {
   const endAddressText = storageMemory
     ? (addressOverflow ? `0x${padStorageHex(addressMax, storageAddressWidth)}+` : `0x${padStorageHex(endAddress, storageAddressWidth)}`)
     : (addressOverflow ? `0x${padWordHex(addressMax)}+` : `0x${padWordHex(endAddress)}`)
+  const scalarRegister = storageScalarGroup ? (registers[0] || null) : null
+  const scalarDefinitionText = scalarRegister ? createScalarDefinitionText(scalarRegister, baseGroup) : ''
+  const scalarRawHexText = scalarRegister ? scalarRegister.rawValueText : ''
+  const scalarValueText = scalarRegister ? formatScalarDecimalValue(scalarRegister) : ''
   const displayName = storageMemory
     ? stripStorageAreaPrefix(baseGroup.name)
     : baseGroup.name
   const listMetaText = storageMemory
-    ? `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B`
+    ? (storageScalarGroup
+      ? [
+        getStorageAreaText(baseGroup),
+        startAddressText,
+        `${byteLength}B`,
+        scalarRawHexText,
+        scalarDefinitionText
+      ].filter(Boolean).join(' ')
+      : `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B`)
     : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}`
   const detailMetaText = storageMemory
     ? `${getStorageAreaText(baseGroup, true)} ${startAddressText} ${quantity}/${byteLength}B`
@@ -734,6 +797,7 @@ function normalizeGroup(group) {
     endAddressText,
     functionCode: registerType.functionCode,
     isReadOnly: !registerType.writable,
+    isStorageScalarGroup: storageScalarGroup,
     byteLength,
     listMetaText,
     maxQuantity,
@@ -741,6 +805,9 @@ function normalizeGroup(group) {
     registerTypeText: registerType.label,
     registers,
     paddedByteLength,
+    scalarDefinitionText,
+    scalarRawHexText,
+    scalarValueText,
     sourceMetaText,
     startAddressText,
     wordQuantity,
@@ -859,6 +926,7 @@ module.exports = {
   isByteRegister,
   isParameterGroupPollEnabled,
   isStorageMemoryGroup,
+  isStorageScalarGroup,
   isStorageStructGroup,
   isTextRegister,
   normalizeGroup,

+ 13 - 1
domain/parameter-groups/struct-c-syntax.js

@@ -2,7 +2,7 @@ const TYPE_ALIASES = {
   bit: 'uint8_t',
   bool: 'uint8_t',
   char: 'int8_t',
-  double: 'float',
+  double: 'double',
   float: 'float',
   int: 'int16_t',
   int8: 'int8_t',
@@ -11,6 +11,12 @@ const TYPE_ALIASES = {
   int16_t: 'int16_t',
   int32: 'int32_t',
   int32_t: 'int32_t',
+  int64: 'int64_t',
+  int64_t: 'int64_t',
+  int128: 'int128_t',
+  int128_t: 'int128_t',
+  int256: 'int256_t',
+  int256_t: 'int256_t',
   long: 'int32_t',
   short: 'int16_t',
   'signed char': 'int8_t',
@@ -23,6 +29,12 @@ const TYPE_ALIASES = {
   uint16_t: 'uint16_t',
   uint32: 'uint32_t',
   uint32_t: 'uint32_t',
+  uint64: 'uint64_t',
+  uint64_t: 'uint64_t',
+  uint128: 'uint128_t',
+  uint128_t: 'uint128_t',
+  uint256: 'uint256_t',
+  uint256_t: 'uint256_t',
   'unsigned char': 'uint8_t',
   'unsigned int': 'uint16_t',
   'unsigned long': 'uint32_t',

+ 6 - 1
domain/parameter-groups/struct-layout.js

@@ -33,7 +33,12 @@ function isAsciiArray(typeText, dataType, name, arrayLength) {
 }
 
 function getDataTypeByteLength(dataType) {
+  if (dataType === 'int8_t' || dataType === 'uint8_t') return 1
+  if (dataType === 'int16_t' || dataType === 'uint16_t') return 2
   if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
+  if (dataType === 'double' || dataType === 'int64_t' || dataType === 'uint64_t') return 8
+  if (dataType === 'int128_t' || dataType === 'uint128_t') return 16
+  if (dataType === 'int256_t' || dataType === 'uint256_t') return 32
   if (dataType === 'int8_t' || dataType === 'uint8_t') return 1
 
   return 2
@@ -161,7 +166,7 @@ function createRegisterFromField(field, dataType, originalTypeText, layoutState,
 
     return [{
       byteStart,
-      dataType: 'ascii',
+      dataType: 'text',
       ...createEnumMeta(enumInfo),
       name: field.name,
       textByteLength: String(arrayLength)

+ 13 - 4
domain/parameter-groups/value-codec.js

@@ -39,6 +39,9 @@ const {
   bytesToFloatValue,
   bytesToSignedInteger,
   bytesToUnsignedInteger,
+  dataTypeIsFloat,
+  dataTypeIsSignedInteger,
+  dataTypeIsWideInteger,
   floatToBytes,
   formatFloatValue,
   formatHexValue,
@@ -233,7 +236,8 @@ function encodeRegisterBytes(register) {
   if (numberValue === null) return null
   validateNumericValue(register, numberValue)
 
-  if (dataType === 'float') return floatToBytes(numberValue, memoryEndian)
+  if (dataTypeIsFloat(dataType)) return floatToBytes(numberValue, memoryEndian, byteLength)
+  if (dataTypeIsWideInteger(dataType)) return unsignedIntegerToBytes(numberValue, byteLength, memoryEndian)
 
   const rounded = Math.round(numberValue)
   if (dataType === 'int8_t' || dataType === 'uint8_t') return [rounded & 0xFF]
@@ -277,8 +281,8 @@ function decodeRegisterValue(register, words) {
   if (isRawRegister(dataType)) {
     return bytes.slice(0, byteLength)
   }
-  if (dataType === 'float') {
-    return bytesToFloatValue(bytes, memoryEndian)
+  if (dataTypeIsFloat(dataType)) {
+    return bytesToFloatValue(bytes, memoryEndian, byteLength)
   }
   if (dataType === 'int8_t') {
     const byteValue = bytes[0] & 0xFF
@@ -300,6 +304,11 @@ function decodeRegisterValue(register, words) {
   if (dataType === 'int32_t') {
     return bytesToSignedInteger(bytes.slice(0, 4), memoryEndian)
   }
+  if (dataTypeIsWideInteger(dataType)) {
+    return dataTypeIsSignedInteger(dataType)
+      ? bytesToSignedInteger(bytes.slice(0, byteLength), memoryEndian)
+      : bytesToUnsignedInteger(bytes.slice(0, byteLength), memoryEndian)
+  }
 
   return bytesToUnsignedInteger(bytes.slice(0, 4), memoryEndian)
 }
@@ -315,7 +324,7 @@ function formatRegisterValue(register, rawValue) {
 
   if (isTextRegister(dataType)) return normalizeTextValue(rawValue)
   if (dataType === 'hex') return formatHexValue(rawValue)
-  if (dataType === 'float') return formatFloatValue(rawValue)
+  if (dataTypeIsFloat(dataType)) return formatFloatValue(rawValue)
 
   return formatIntegerValue(rawValue, dataType)
 }

+ 92 - 28
domain/parameter-groups/value-number.js

@@ -10,8 +10,50 @@ function padWordHex(value) {
   return Number(value || 0).toString(16).toUpperCase().padStart(4, '0')
 }
 
+function dataTypeIsFloat(dataType) {
+  const type = getDataType(dataType).key
+
+  return type === 'float' || type === 'double'
+}
+
+function dataTypeIsSignedInteger(dataType) {
+  return /^int(?:8|16|32|64|128|256)_t$/.test(getDataType(dataType).key)
+}
+
+function dataTypeIsWideInteger(dataType) {
+  return /^(?:u?int)(?:64|128|256)_t$/.test(getDataType(dataType).key)
+}
+
+function getIntegerBitWidth(dataType) {
+  const match = getDataType(dataType).key.match(/^(?:u?int)(\d+)_t$/)
+
+  return match ? Number(match[1]) : 0
+}
+
+function normalizeBigIntValue(value) {
+  if (typeof value === 'bigint') return value
+
+  const text = String(value === undefined || value === null ? '' : value).trim()
+  if (!text) return null
+
+  try {
+    if (/^[-+]0x/i.test(text)) {
+      const sign = text[0] === '-' ? -1n : 1n
+
+      return sign * BigInt(`0x${text.slice(3)}`)
+    }
+
+    return BigInt(text)
+  } catch (error) {
+    return null
+  }
+}
+
 function formatIntegerValue(value, dataType) {
   const type = getDataType(dataType).key
+  const bigValue = normalizeBigIntValue(value)
+  if (dataTypeIsWideInteger(type)) return bigValue === null ? '--' : bigValue.toString()
+
   const numberValue = Number(value)
 
   if (!Number.isFinite(numberValue)) return '--'
@@ -80,6 +122,15 @@ function getNumericRange(dataType) {
   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 (dataTypeIsWideInteger(type)) {
+    const width = getIntegerBitWidth(type)
+    const max = dataTypeIsSignedInteger(type)
+      ? (1n << BigInt(width - 1)) - 1n
+      : (1n << BigInt(width)) - 1n
+    const min = dataTypeIsSignedInteger(type) ? -(1n << BigInt(width - 1)) : 0n
+
+    return { max, min }
+  }
   if (type === 'hex') return { max: 0xFFFF, min: 0 }
 
   return { max: Number.POSITIVE_INFINITY, min: Number.NEGATIVE_INFINITY }
@@ -89,11 +140,12 @@ function parseNumberText(value, dataType) {
   const text = String(value === undefined || value === null ? '' : value).trim()
   if (!text || text === '--') return null
 
-  if (getDataType(dataType).key === 'float') {
+  if (dataTypeIsFloat(dataType)) {
     const parsed = Number(text)
     return Number.isFinite(parsed) ? parsed : null
   }
   if (isHexRegister(dataType)) return parseHexText(text)
+  if (dataTypeIsWideInteger(dataType)) return normalizeBigIntValue(text)
 
   return parseIntegerText(text)
 }
@@ -113,10 +165,12 @@ function parseRangeBoundary(value, dataType, label) {
 function validateNumericValue(register, value) {
   const dataType = getDataType(register.dataType).key
   const range = getNumericRange(dataType)
-  const numberValue = Number(value)
-  if (!Number.isFinite(numberValue)) return false
+  const isWideInteger = dataTypeIsWideInteger(dataType)
+  const numberValue = isWideInteger ? normalizeBigIntValue(value) : Number(value)
+  if (isWideInteger && numberValue === null) return false
+  if (!isWideInteger && !Number.isFinite(numberValue)) return false
 
-  if (dataType !== 'float' && Math.round(numberValue) !== numberValue) {
+  if (!dataTypeIsFloat(dataType) && !isWideInteger && Math.round(numberValue) !== numberValue) {
     throw new Error(`${register.name || '寄存器'} 需要整数`)
   }
   if (numberValue < range.min || numberValue > range.max) {
@@ -135,66 +189,76 @@ function validateNumericValue(register, value) {
   return true
 }
 
-function floatToBytes(value, byteOrder = 'big') {
-  const buffer = new ArrayBuffer(4)
+function floatToBytes(value, byteOrder = 'big', byteLength = 4) {
+  const length = byteLength === 8 ? 8 : 4
+  const buffer = new ArrayBuffer(length)
   const view = new DataView(buffer)
 
-  view.setFloat32(0, Number(value), false)
+  if (length === 8) {
+    view.setFloat64(0, Number(value), false)
+  } else {
+    view.setFloat32(0, Number(value), false)
+  }
 
-  return applyByteOrder([
-    view.getUint8(0),
-    view.getUint8(1),
-    view.getUint8(2),
-    view.getUint8(3)
-  ], byteOrder)
+  return applyByteOrder(Array.from({ length }, (_, index) => view.getUint8(index)), byteOrder)
 }
 
-function bytesToFloatValue(bytes, byteOrder = 'big') {
-  if (!Array.isArray(bytes) || bytes.length < 4) return null
+function bytesToFloatValue(bytes, byteOrder = 'big', byteLength = 4) {
+  const length = byteLength === 8 ? 8 : 4
+  if (!Array.isArray(bytes) || bytes.length < length) return null
 
-  const buffer = new ArrayBuffer(4)
+  const buffer = new ArrayBuffer(length)
   const view = new DataView(buffer)
-  const source = applyByteOrder(bytes.slice(0, 4), byteOrder)
+  const source = applyByteOrder(bytes.slice(0, length), byteOrder)
 
-  for (let index = 0; index < 4; index += 1) {
+  for (let index = 0; index < length; index += 1) {
     view.setUint8(index, Number(source[index]) & 0xFF)
   }
 
-  return view.getFloat32(0, false)
+  return length === 8 ? view.getFloat64(0, false) : view.getFloat32(0, false)
 }
 
 function unsignedIntegerToBytes(value, byteLength, byteOrder = 'big') {
-  let numberValue = Math.round(Number(value) || 0)
+  let numberValue = normalizeBigIntValue(value)
+  if (numberValue === null) numberValue = 0n
   const bytes = []
 
   if (numberValue < 0) {
-    numberValue += Math.pow(2, byteLength * 8)
+    numberValue += 1n << BigInt(byteLength * 8)
   }
 
   for (let index = byteLength - 1; index >= 0; index -= 1) {
-    bytes[index] = numberValue & 0xFF
-    numberValue = Math.floor(numberValue / 0x100)
+    bytes[index] = Number(numberValue & 0xFFn)
+    numberValue >>= 8n
   }
 
   return applyByteOrder(bytes, byteOrder)
 }
 
 function bytesToUnsignedInteger(bytes, byteOrder = 'big') {
-  return applyByteOrder(bytes, byteOrder).reduce((value, byte) => ((value * 0x100) + (Number(byte) & 0xFF)), 0)
+  const source = applyByteOrder(bytes, byteOrder)
+  const bigValue = source.reduce((value, byte) => ((value << 8n) + BigInt(Number(byte) & 0xFF)), 0n)
+
+  return source.length > 4 ? bigValue.toString() : Number(bigValue)
 }
 
 function bytesToSignedInteger(bytes, byteOrder = 'big') {
-  const unsignedValue = bytesToUnsignedInteger(bytes, byteOrder)
-  const signLimit = Math.pow(2, bytes.length * 8 - 1)
-  const fullRange = Math.pow(2, bytes.length * 8)
+  const source = applyByteOrder(bytes, byteOrder)
+  const unsignedValue = source.reduce((value, byte) => ((value << 8n) + BigInt(Number(byte) & 0xFF)), 0n)
+  const signLimit = 1n << BigInt(source.length * 8 - 1)
+  const fullRange = 1n << BigInt(source.length * 8)
+  const signedValue = unsignedValue >= signLimit ? unsignedValue - fullRange : unsignedValue
 
-  return unsignedValue >= signLimit ? unsignedValue - fullRange : unsignedValue
+  return source.length > 4 ? signedValue.toString() : Number(signedValue)
 }
 
 module.exports = {
   bytesToFloatValue,
   bytesToSignedInteger,
   bytesToUnsignedInteger,
+  dataTypeIsFloat,
+  dataTypeIsSignedInteger,
+  dataTypeIsWideInteger,
   floatToBytes,
   formatFloatValue,
   formatHexValue,

+ 3 - 0
domain/parameter-groups/value-text.js

@@ -2,6 +2,7 @@ const {
   normalizeTextValue
 } = require('../../utils/base-utils.js')
 const {
+  bytesToAutoText,
   trimTrailingNullBytes
 } = require('../../utils/binary-utils.js')
 const {
@@ -86,6 +87,8 @@ function encodeTextBytes(text, dataType, byteLimit = MAX_TEXT_BYTE_LENGTH) {
 function decodeTextBytes(bytes, dataType) {
   const normalizedType = getDataType(dataType).key
 
+  if (normalizedType === 'text') return bytesToAutoText(bytes)
+
   return normalizedType === 'ascii'
     ? decodeAsciiBytes(bytes)
     : decodeUtf8Bytes(bytes)

+ 13 - 2
domain/parameter-groups/value-types.js

@@ -7,7 +7,9 @@ const {
 } = require('./constants.js')
 
 function getDataType(dataType) {
-  return DATA_TYPE_OPTIONS.find((item) => item.key === dataType)
+  const normalizedType = dataType === 'ascii' || dataType === 'utf8' ? 'text' : dataType
+
+  return DATA_TYPE_OPTIONS.find((item) => item.key === normalizedType)
     || DATA_TYPE_OPTIONS.find((item) => item.key === DEFAULT_DATA_TYPE)
     || DATA_TYPE_OPTIONS[0]
 }
@@ -55,6 +57,13 @@ function isStructLayout(layout) {
   return layout === GROUP_LAYOUT_STRUCT
 }
 
+function isByteAddressedTextRegister(register = {}) {
+  const addressUnit = String(register.addressUnit || '').trim().toLowerCase()
+  const memoryArea = String(register.sourceMemoryArea || '').trim()
+
+  return addressUnit === 'byte' || addressUnit === 'bytes' || !!memoryArea
+}
+
 function isBitFieldRegister(register = {}) {
   return !!register.isBitField
 }
@@ -92,7 +101,9 @@ function getRegisterByteLength(dataType, register = {}) {
   if (type.kind === 'text') {
     const byteLength = getRegisterTextByteLength(register)
 
-    return isStructLayout(register.layout) ? byteLength : alignEvenByteLength(byteLength)
+    return isStructLayout(register.layout) || isByteAddressedTextRegister(register)
+      ? byteLength
+      : alignEvenByteLength(byteLength)
   }
 
   return type.byteLength || ((type.wordCount || 1) * 2)

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 729 - 138
domain/storage-access/code-info-parser.js


+ 192 - 12
features/parameter-groups/imports.js

@@ -13,8 +13,11 @@ function getRegisterByteLengthFromConfig(register) {
   if (dataType === 'raw') {
     return Math.max(1, Number(register.sourceByteLength || register.byteLength || register.rawByteLength) || 1)
   }
-  if (dataType === 'ascii' || dataType === 'utf8') return Math.max(1, Number(register.textByteLength) || 1)
+  if (dataType === 'text') return Math.max(1, Number(register.textByteLength) || 1)
   if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
+  if (dataType === 'double' || dataType === 'int64_t' || dataType === 'uint64_t') return 8
+  if (dataType === 'int128_t' || dataType === 'uint128_t') return 16
+  if (dataType === 'int256_t' || dataType === 'uint256_t') return 32
   if (dataType === 'int8_t' || dataType === 'uint8_t') return 1
 
   return 2
@@ -49,7 +52,7 @@ function normalizeSymbolText(value) {
 }
 
 function getStorageStructTypeName(group = {}) {
-  return String(group.sourceSymbolType || group.sourceSymbolName || group.name || '')
+  return String(group.sourceDefinitionName || group.sourceSymbolType || group.sourceSymbolName || group.name || '')
 }
 
 function isStructCodeInfoEntry(group = {}) {
@@ -58,15 +61,37 @@ function isStructCodeInfoEntry(group = {}) {
   return !entryKind || entryKind === 'struct'
 }
 
+function isEnumCodeInfoEntry(group = {}) {
+  return String(group.sourceEntryKind || '').trim().toLowerCase() === 'enum'
+}
+
+function isArrayCodeInfoEntry(group = {}) {
+  return String(group.sourceEntryKind || '').trim().toLowerCase() === 'array'
+}
+
 function isVariableCodeInfoEntry(group = {}) {
   return String(group.sourceEntryKind || '').trim().toLowerCase() === 'variable'
 }
 
+function isStructArrayCodeInfoEntry(group = {}) {
+  return isArrayCodeInfoEntry(group)
+    && String(group.sourceElementType || group.sourceValueType || '').trim().toLowerCase() === 'struct'
+}
+
+function isEnumArrayCodeInfoEntry(group = {}) {
+  return isArrayCodeInfoEntry(group)
+    && String(group.sourceElementType || group.sourceValueType || '').trim().toLowerCase() === 'enum'
+}
+
 function structDefinitionNameMatches(group = {}, structInfo = {}) {
   const expectedName = normalizeSymbolText(getStorageStructTypeName(group))
-  const structName = normalizeSymbolText(structInfo.name)
+  const structNames = [
+    structInfo.name,
+    structInfo.tagName,
+    structInfo.tagName ? `struct ${structInfo.tagName}` : ''
+  ].map(normalizeSymbolText).filter(Boolean)
 
-  return !!expectedName && !!structName && expectedName === structName
+  return !!expectedName && structNames.indexOf(expectedName) >= 0
 }
 
 function findStructCompletion(group, catalog) {
@@ -120,13 +145,21 @@ function findStructCompletion(group, catalog) {
 function getEnumLookupNames(group = {}) {
   const registers = Array.isArray(group.registers) ? group.registers : []
   const names = [
+    group.sourceDefinitionName,
     group.sourceSymbolType,
     group.sourceSymbolName,
+    group.sourceInstanceName,
     group.name
   ]
 
   registers.forEach((register) => {
-    names.push(register.sourceSymbolType, register.sourceSymbolName, register.name)
+    names.push(
+      register.sourceDefinitionName,
+      register.sourceSymbolType,
+      register.sourceSymbolName,
+      register.sourceInstanceName,
+      register.name
+    )
   })
 
   return names.map(normalizeSymbolText).filter(Boolean)
@@ -134,11 +167,22 @@ function getEnumLookupNames(group = {}) {
 
 function findEnumCompletion(group, catalog = {}) {
   const enums = Array.isArray(catalog.enums) ? catalog.enums : []
-  if (!isVariableCodeInfoEntry(group) || !enums.length) return null
+  if ((!isEnumCodeInfoEntry(group) && !isVariableCodeInfoEntry(group) && !isEnumArrayCodeInfoEntry(group)) || !enums.length) return null
 
   const names = getEnumLookupNames(group)
   if (!names.length) return null
 
+  if (isEnumCodeInfoEntry(group) || isEnumArrayCodeInfoEntry(group)) {
+    const matchedEnum = enums.find((enumInfo) => (
+      [enumInfo.name, enumInfo.typedefName, enumInfo.tagName]
+        .concat(enumInfo.typeNames || [])
+        .map(normalizeSymbolText)
+        .filter(Boolean)
+        .some((name) => names.indexOf(name) >= 0)
+    ))
+    if (matchedEnum) return matchedEnum
+  }
+
   const enumVariablesByName = catalog.enumVariablesByName || {}
   for (const name of names) {
     const variableEnum = enumVariablesByName[name]
@@ -159,10 +203,32 @@ function getIntegerDataTypeForByteLength(byteLength) {
   if (length === 1) return 'uint8_t'
   if (length === 2) return 'uint16_t'
   if (length === 4) return 'uint32_t'
+  if (length === 8) return 'uint64_t'
+  if (length === 16) return 'uint128_t'
+  if (length === 32) return 'uint256_t'
 
   return ''
 }
 
+function getDataTypeByteLength(dataType) {
+  const key = getDataType(dataType).key
+  if (key === 'int8_t' || key === 'uint8_t') return 1
+  if (key === 'int16_t' || key === 'uint16_t' || key === 'hex') return 2
+  if (key === 'int32_t' || key === 'uint32_t' || key === 'float') return 4
+  if (key === 'double' || key === 'int64_t' || key === 'uint64_t') return 8
+  if (key === 'int128_t' || key === 'uint128_t') return 16
+  if (key === 'int256_t' || key === 'uint256_t') return 32
+
+  return 2
+}
+
+function getEnumDataTypeForByteLength(enumInfo, byteLength) {
+  const inferredType = getDataType(enumInfo && enumInfo.dataType).key
+  if (getDataTypeByteLength(inferredType) === Number(byteLength)) return inferredType
+
+  return getIntegerDataTypeForByteLength(byteLength)
+}
+
 function cloneEnumOptions(enumInfo) {
   return (Array.isArray(enumInfo && enumInfo.options) ? enumInfo.options : []).map((option) => ({
     label: option.label || option.name,
@@ -179,9 +245,8 @@ function completeEnumVariableGroup(group, enumInfo) {
 
   const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
   const registers = sourceRegisters.map((register) => {
-    const dataType = getIntegerDataTypeForByteLength(
-      register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength
-    )
+    const byteLength = register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength
+    const dataType = getEnumDataTypeForByteLength(enumInfo, byteLength)
 
     return dataType
       ? {
@@ -189,6 +254,7 @@ function completeEnumVariableGroup(group, enumInfo) {
         dataType,
         enumName: enumInfo.name,
         enumOptions,
+        sourceDefinitionName: enumInfo.name || register.sourceDefinitionName,
         sourceSymbolType: enumInfo.name || register.sourceSymbolType
       }
       : register
@@ -221,7 +287,9 @@ function createCompletedRegisters(group, completion) {
       sourceAddressByteLength: group.sourceAddressByteLength,
       sourceAddressText: formatAddress(sourceAddress, group.sourceAddressWidth),
       sourceAddressWidth: group.sourceAddressWidth,
+      sourceDefinitionName: group.sourceDefinitionName,
       sourceEntryKind: group.sourceEntryKind,
+      sourceInstanceName: group.sourceInstanceName,
       sourceMemoryArea: group.sourceMemoryArea,
       sourceMemoryClass: group.sourceMemoryClass,
       sourceSymbolName: group.sourceSymbolName,
@@ -230,6 +298,100 @@ function createCompletedRegisters(group, completion) {
   })
 }
 
+function getArrayDimensions(group = {}) {
+  return (Array.isArray(group.sourceArrayDimensions) ? group.sourceArrayDimensions : [])
+    .map((value) => Math.max(1, Math.floor(Number(value) || 1)))
+    .filter((value) => value > 0)
+}
+
+function getArrayIndexPath(linearIndex, dimensions = []) {
+  const safeDimensions = getArrayDimensions({ sourceArrayDimensions: dimensions })
+  if (!safeDimensions.length) return [Math.max(0, Math.floor(Number(linearIndex) || 0))]
+
+  const indexes = Array.from({ length: safeDimensions.length }, () => 0)
+  let remaining = Math.max(0, Math.floor(Number(linearIndex) || 0))
+  for (let index = safeDimensions.length - 1; index >= 0; index -= 1) {
+    indexes[index] = remaining % safeDimensions[index]
+    remaining = Math.floor(remaining / safeDimensions[index])
+  }
+
+  return indexes
+}
+
+function formatArrayIndexPath(indexPath = []) {
+  return (Array.isArray(indexPath) ? indexPath : [indexPath])
+    .map((index) => `[${Math.max(0, Math.floor(Number(index) || 0))}]`)
+    .join('')
+}
+
+function getStructArrayElementCount(group = {}) {
+  const explicitCount = Number(group.sourceElementCount)
+  if (Number.isFinite(explicitCount) && explicitCount > 0) return Math.floor(explicitCount)
+
+  const dimensions = getArrayDimensions(group)
+  if (dimensions.length) return dimensions.reduce((total, value) => total * value, 1)
+
+  return 1
+}
+
+function createCompletedStructArrayRegisters(group, completion) {
+  const existingRemarksByByteStart = (Array.isArray(group.registers) ? group.registers : []).reduce((remarks, register) => {
+    const byteStart = Number(register && register.byteStart)
+    const remark = String(register && register.remark ? register.remark : '').trim()
+    if (Number.isFinite(byteStart) && remark) remarks[Math.floor(byteStart)] = remark
+
+    return remarks
+  }, {})
+  const elementCount = getStructArrayElementCount(group)
+  const elementByteLength = Math.max(1, Math.floor(Number(group.sourceElementByteLength) || getRegistersByteLength(completion.registers) || 1))
+  const dimensions = getArrayDimensions(group)
+  const baseAddress = Number(group.sourceAddress) || Number(group.startAddress) || 0
+  const baseName = group.sourceSymbolName || group.name || completion.name
+  const registers = []
+
+  for (let elementIndex = 0; elementIndex < elementCount; elementIndex += 1) {
+    const elementByteStart = elementIndex * elementByteLength
+    const indexPath = getArrayIndexPath(elementIndex, dimensions)
+    const indexText = formatArrayIndexPath(indexPath)
+
+    completion.registers.forEach((register) => {
+      const fieldByteStart = getRegisterByteStart(register)
+      const byteStart = elementByteStart + fieldByteStart
+      const sourceAddress = baseAddress + byteStart
+      const fieldName = register.name || `field_${fieldByteStart}`
+
+      registers.push({
+        ...register,
+        byteStart,
+        isStructField: true,
+        name: `${baseName}${indexText}.${fieldName}`,
+        remark: register.remark || existingRemarksByByteStart[Math.floor(byteStart)] || '',
+        sourceAddress,
+        sourceAddressByteLength: group.sourceAddressByteLength,
+        sourceAddressText: formatAddress(sourceAddress, group.sourceAddressWidth),
+        sourceAddressWidth: group.sourceAddressWidth,
+        sourceArrayDimensions: dimensions,
+        sourceArrayIndex: elementIndex,
+        sourceArrayIndexPath: indexPath,
+        sourceByteLength: register.sourceByteLength,
+        sourceDefinitionName: group.sourceDefinitionName,
+        sourceElementByteLength: elementByteLength,
+        sourceElementCount: elementCount,
+        sourceElementType: group.sourceElementType,
+        sourceEntryKind: group.sourceEntryKind,
+        sourceInstanceName: group.sourceInstanceName,
+        sourceMemoryArea: group.sourceMemoryArea,
+        sourceMemoryClass: group.sourceMemoryClass,
+        sourceSymbolName: `${baseName}${indexText}.${fieldName}`,
+        sourceSymbolType: completion.structName || register.sourceSymbolType,
+        sourceValueType: group.sourceValueType
+      })
+    })
+  }
+
+  return registers
+}
+
 function completeStructInstanceGroups(groups, sourceText, options = {}) {
   const catalog = parseStructCatalog(sourceText)
   let completedCount = 0
@@ -237,14 +399,14 @@ function completeStructInstanceGroups(groups, sourceText, options = {}) {
 
   const nextGroups = groups.map((group) => {
     if (!group.sourceSymbolName || !group.sourceMemoryArea) return group
-    if (isVariableCodeInfoEntry(group)) {
+    if (isEnumCodeInfoEntry(group) || isVariableCodeInfoEntry(group) || isEnumArrayCodeInfoEntry(group)) {
       const enumInfo = findEnumCompletion(group, catalog)
       if (!enumInfo) return group
 
       completedCount += 1
       return completeEnumVariableGroup(group, enumInfo)
     }
-    if (!isStructCodeInfoEntry(group)) return group
+    if (!isStructCodeInfoEntry(group) && !isStructArrayCodeInfoEntry(group)) return group
 
     const completion = findStructCompletion(group, catalog)
     if (!completion || !completion.registers || !completion.registers.length) {
@@ -254,12 +416,26 @@ function completeStructInstanceGroups(groups, sourceText, options = {}) {
 
     const expectedBytes = Number(group.sourceByteLength || group.byteLength || 0)
     const actualBytes = getRegistersByteLength(completion.registers)
-    if (expectedBytes > 0 && actualBytes !== expectedBytes && options.strictLength !== false) {
+    const expectedElementBytes = Number(group.sourceElementByteLength || 0)
+    const expectedStructBytes = isStructArrayCodeInfoEntry(group) && expectedElementBytes > 0
+      ? expectedElementBytes
+      : expectedBytes
+    if (expectedStructBytes > 0 && actualBytes !== expectedStructBytes && options.strictLength !== false) {
       skippedCount += 1
       return group
     }
 
     completedCount += 1
+    if (isStructArrayCodeInfoEntry(group)) {
+      const registers = createCompletedStructArrayRegisters(group, completion)
+
+      return normalizeGroup({
+        ...group,
+        layout: 'struct',
+        quantity: registers.length,
+        registers
+      })
+    }
 
     return normalizeGroup({
       ...group,
@@ -338,6 +514,8 @@ function getRegisterDuplicateKey(register = {}, group = {}) {
 }
 
 function isSingleRegisterAggregateGroup(group = {}) {
+  if (isArrayCodeInfoEntry(group)) return false
+
   const groupSymbolName = normalizeDuplicateText(group.sourceSymbolName || group.name || '')
   const registers = Array.isArray(group.registers) ? group.registers : []
 
@@ -516,7 +694,9 @@ function mergePreservedStructRegister(register = {}, incomingGroup = {}) {
     sourceAddressByteLength: incomingGroup.sourceAddressByteLength || register.sourceAddressByteLength,
     sourceAddressText: formatAddress(sourceAddress, incomingGroup.sourceAddressWidth || register.sourceAddressWidth),
     sourceAddressWidth: incomingGroup.sourceAddressWidth || register.sourceAddressWidth,
+    sourceDefinitionName: incomingGroup.sourceDefinitionName || register.sourceDefinitionName,
     sourceEntryKind: incomingGroup.sourceEntryKind,
+    sourceInstanceName: incomingGroup.sourceInstanceName || register.sourceInstanceName,
     sourceMemoryArea: incomingGroup.sourceMemoryArea,
     sourceMemoryClass: incomingGroup.sourceMemoryClass,
     sourceSymbolName,

+ 2 - 0
features/parameter-groups/service.js

@@ -203,6 +203,7 @@ async function completeStructInstanceGroupsWithStructFile(options = {}) {
 
     return {
       completedCount: 0,
+      enumCount: 0,
       skippedCount: 0,
       structCount: 0,
       variableCount: 0
@@ -237,6 +238,7 @@ async function syncFromStorageAccessCodeInfo(options = {}) {
   if (!result || !result.ok) return result
 
   setStorageCodeInfo(result.codeInfo)
+  settingsService.setStorageAccessDefaultEndian(result.codeInfoMemoryEndian || result.codeInfo && result.codeInfo.memoryEndian)
   const merged = mergeImportedGroupsIntoState(result.importedGroups || [], {
     preserveExistingRemarks: true,
     preserveExistingPollEnabled: true,

+ 3 - 3
features/parameter-groups/view-model.js

@@ -14,7 +14,7 @@ function getCodeInfoVariableByteLength(dialog = {}) {
   const entryKind = String(dialog.sourceEntryKind || '').trim().toLowerCase()
   const byteLength = Number(dialog.sourceByteLength)
 
-  if (entryKind !== 'variable' || !Number.isFinite(byteLength) || byteLength <= 0) return 0
+  if ((entryKind !== 'variable' && entryKind !== 'enum') || !Number.isFinite(byteLength) || byteLength <= 0) return 0
 
   return Math.floor(byteLength)
 }
@@ -31,12 +31,12 @@ function validateCodeInfoVariableDataType(dialog = {}, dataType = {}) {
   if (!sourceByteLength || dataType.kind === 'raw') return
 
   if (dataType.kind === 'text' || dataType.kind === 'hex') {
-    throw new Error('单独变量类型请选择有符号/无符号整数、float 或 enum 对应整数类型')
+    throw new Error('变量类型请选择整数、浮点或 enum 对应整数类型')
   }
 
   const dataTypeByteLength = getDataTypeConfigByteLength(dataType, dialog)
   if (dataTypeByteLength !== sourceByteLength) {
-    throw new Error(`单独变量 TLV 长度为 ${sourceByteLength}B,不能选择 ${dataType.label || dataType.key || '该类型'}`)
+    throw new Error(`变量 TLV 长度为 ${sourceByteLength}B,不能选择 ${dataType.label || dataType.key || '该类型'}`)
   }
 }
 

+ 3 - 2
features/settings/protocol-implementation.js

@@ -14,8 +14,9 @@ const GUIDE = {
     { id: 'frame', text: 'CMD bit7 为故障位,仅回帧出现;bit6 为特殊指令位;普通读写中 bit3 为读写位,bit4/bit5 保留,bit0~bit2 表示区域。' },
     { id: 'addr', text: '普通区域 0x00 为 codeinfo 只读描述符;0x01/0x02/0x03/0x04 为 DATA、IDATA、XDATA、CODE 的 16 位地址;0x07 为 ADDR32 的 32 位地址;0x05~0x06 保留。' },
     { id: 'info', text: 'bit6=1 时 bit0~bit5 表示特殊指令码;当前仅定义 0x01 复位,因此复位帧 CMD=0x41。' },
-    { id: 'control', text: 'CodeInfo 同步先发送 00+CRC 读取 area=0x00 描述符,数据区返回小端序 TLV 起始 addr32、len16、地址位宽、最大包长和原始 ENDIAN_MARK(55AA/AA55)。' },
-    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;0x01~0x08 为固定内存入口,VALUE 为小端序 addr(2/4)+len16+name_len8+name;单独变量由 TLV 给长度,UI 或 enum 导入配置解释类型。' },
+    { id: 'control', text: 'CodeInfo 同步先发送 00+CRC 读取 area=0x00 描述符,数据区返回 TLV 起始 addr32、len16、地址位宽、最大包长和原始 ENDIAN_MARK(55AA/AA55),除 CRC 外的多字节字段都跟随该标记解析。' },
+    { id: 'endian', text: '未同步 CodeInfo 时使用设置页“默认大端模式”开关;同步成功后小程序会按 ENDIAN_MARK 自动切换该开关。' },
+    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;TYPE 直接定义基础变量、数组、enum 或结构体入口。16 位入口 VALUE 携带 DATA/IDATA/XDATA/CODE 区域字节,32 位入口固定为统一地址。' },
     { id: 'bootloader', text: 'Bootloader 升级前先发送存储访问复位帧 CMD=0x41,再在 500ms 内每 50ms 发送一次 Bootloader 握手帧。' },
     { id: 'source', text: '协议实现源码已暂时移除,设置页仅保留文件占位与说明,避免给出过期从机实现。' }
   ]

+ 6 - 0
features/settings/view-model.js

@@ -25,6 +25,9 @@ function getSettingsPageState(
   const isNoProtocol = settingsService.isNoProtocol(protocol.key)
   const isModbusProtocol = settingsService.isModbusProtocol(protocol.key)
   const isStorageAccessProtocol = settingsService.isStorageAccessProtocol(protocol.key)
+  const storageAccessDefaultEndian = settingsService.normalizeStorageAccessEndian(
+    settingsState.storageAccessDefaultEndian
+  )
 
   return {
     ...settingsState,
@@ -42,6 +45,9 @@ function getSettingsPageState(
     storageProtocolImplementationEntryMeta: '从机实现与结构体定义参考',
     storageProtocolImplementationView: protocolImplementation.VIEW_ID,
     protocolOptions,
+    storageAccessDefaultEndian,
+    storageAccessDefaultEndianBigSwitch: storageAccessDefaultEndian === 'big',
+    storageAccessDefaultEndianText: storageAccessDefaultEndian === 'big' ? '大端' : '小端',
     protocolText: protocol.label,
     toolEntries: toolNavigation.getToolEntries()
   }

+ 7 - 2
features/storage-access/code-info-sync.js

@@ -62,7 +62,7 @@ async function readCodeInfoBlock(label = '同步CodeInfo', kind = 'storage-code-
   const codeInfoAddress = Number(descriptor.codeInfoAddress || 0)
   const codeInfoByteLength = Number(descriptor.codeInfoByteLength || 0)
   const codeInfoMaxPacketLength = Number(descriptor.codeInfoMaxPacketLength || 0) & 0xFFFF
-  const codeInfoMemoryEndian = String(descriptor.codeInfoMemoryEndian || 'big').trim()
+  const codeInfoMemoryEndian = String(descriptor.codeInfoMemoryEndian || 'little').trim()
   const codeInfoMemoryType = codeInfoAddressWidth === 32
     ? storageAccessProtocol.AREA.ADDR32
     : storageAccessProtocol.AREA.CODE
@@ -86,6 +86,7 @@ async function readCodeInfoBlock(label = '同步CodeInfo', kind = 'storage-code-
     {
       ...protocolIoOptions,
       maxFrameBytes: codeInfoMaxFrameBytes,
+      memoryEndian: codeInfoMemoryEndian,
       useDeviceCaps: false
     }
   )
@@ -158,10 +159,14 @@ async function syncCodeInfo(options = {}) {
     codeInfoMemoryEndian: result.codeInfoMemoryEndian,
     codeInfoMemoryEndianMark: result.codeInfoMemoryEndianMark,
     codeInfoMemoryType: result.codeInfoMemoryType,
+    entryCount: codeInfo.entryCount,
+    enumCount: codeInfo.enumCount,
     groupCount: importedGroups.length,
     importedGroups,
     ok: true,
-    structCount: codeInfo.structCount
+    arrayCount: codeInfo.arrayCount,
+    structCount: codeInfo.structCount,
+    variableCount: codeInfo.variableCount
   }
 }
 

+ 5 - 3
features/storage-access/manual-command.js

@@ -67,9 +67,11 @@ function normalizeDisplayHexText(value, addressWidth = 16) {
 
 function buildMemoryPreview(commandKey, area, address, length, dataBytes) {
   try {
-    const frameOptions = {
-      maxFrameBytes: 0
-    }
+    const frameOptions = protocolIo.normalizeProtocolIoOptions({
+      maxFrameBytes: 0,
+      useDeviceCaps: false,
+      showModal: false
+    })
 
     if (commandKey === 'write') {
       return bytesToHex(storageAccessProtocol.buildWriteFrame(area, address, dataBytes, frameOptions), ' ')

+ 14 - 4
features/storage-access/protocol-io.js

@@ -30,12 +30,16 @@ function resolveMaxPacketLength(value) {
 
 function normalizeProtocolIoOptions(options = {}) {
   const maxPacketLength = options.maxPacketLength === undefined ? options.maxFrameBytes : options.maxPacketLength
+  const settings = settingsService.getState()
+  const optionMemoryEndian = storageAccessProtocol.normalizeMemoryEndian(options.memoryEndian, '')
+  const defaultMemoryEndian = storageAccessProtocol.normalizeMemoryEndian(settings.storageAccessDefaultEndian)
 
   return {
     expectedByteLength: options.expectedByteLength,
     maxFrameBytes: options.useDeviceCaps === false
       ? getConfiguredMaxPacketLength(maxPacketLength)
       : resolveMaxPacketLength(maxPacketLength),
+    memoryEndian: optionMemoryEndian || defaultMemoryEndian,
     onChunk: options.onChunk,
     showModal: options.showModal !== false
   }
@@ -95,10 +99,13 @@ async function readMemory(area, startAddress, byteLength, label, kind, options =
   for (const chunk of chunks) {
     const response = await sendStorageFrame(
       storageAccessProtocol.buildReadFrame(normalizedArea, chunk.address, chunk.quantity, {
-        maxFrameBytes: protocolIoOptions.maxFrameBytes
+        maxFrameBytes: protocolIoOptions.maxFrameBytes,
+        memoryEndian: protocolIoOptions.memoryEndian
       }),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
-      storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind),
+      storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind, {
+        memoryEndian: protocolIoOptions.memoryEndian
+      }),
       protocolIoOptions
     )
     if (!response) return null
@@ -125,10 +132,13 @@ async function writeMemory(area, startAddress, bytes, label, kind, options = {})
   for (const chunk of chunks) {
     const response = await sendStorageFrame(
       storageAccessProtocol.buildWriteFrame(normalizedArea, chunk.address, chunk.dataBytes, {
-        maxFrameBytes: protocolIoOptions.maxFrameBytes
+        maxFrameBytes: protocolIoOptions.maxFrameBytes,
+        memoryEndian: protocolIoOptions.memoryEndian
       }),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
-      storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind),
+      storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind, {
+        memoryEndian: protocolIoOptions.memoryEndian
+      }),
       protocolIoOptions
     )
     if (!response) return false

+ 14 - 0
pages/params/params.js

@@ -190,6 +190,7 @@ Page({
     const groupId = event.currentTarget.dataset.groupId
     const group = findParameterGroup(getParameterGroupsFromState(this.data), groupId)
     if (!group) return
+    if (group.isStorageScalarGroup) return
 
     if (this.parameterGroupLongPressGuard === groupId) {
       this.parameterGroupLongPressGuard = ''
@@ -212,6 +213,19 @@ Page({
     })
   },
 
+  openStorageScalarRegisterEdit(event) {
+    const groupId = event.currentTarget.dataset.groupId
+    const group = findParameterGroup(getParameterGroupsFromState(this.data), groupId)
+    const register = group && group.registers ? group.registers[0] : null
+    if (!group || !group.isStorageScalarGroup || !register) return
+
+    if (this.pageToast) this.pageToast.clear()
+    this.parameterGroupLongPressGuard = groupId
+    this.setData({
+      parameterDialog: createParameterRegisterDialogState('editRegister', group, register, 0)
+    })
+  },
+
   backToParamsHome() {
     if (this.pageToast) this.pageToast.clear()
     this.closeParameterDraft()

+ 43 - 25
pages/params/params.wxml

@@ -52,37 +52,55 @@
           bindtouchend="onParameterGroupTouchEnd"
         >
           <view class="panel-header panel-header--with-actions">
-            <view
-              class="panel-heading-toggle"
-              data-group-id="{{group.id}}"
-              bindtap="openParameterGroup"
-            >
-              <view class="panel-icon icon-terminal">
-                <image class="panel-icon-image" src="/assets/icons/terminal-white.png" mode="aspectFit" />
-              </view>
-              <view class="generic-group-title-wrap">
-                <view class="panel-title" data-group-id="{{group.id}}" catchlongpress="openParameterGroupEdit">{{group.displayName || group.name}}</view>
-                <view class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+            <block wx:if="{{group.isStorageScalarGroup}}">
+              <view class="panel-heading-toggle generic-scalar-heading">
+                <view class="panel-icon icon-terminal">
+                  <image class="panel-icon-image" src="/assets/icons/terminal-white.png" mode="aspectFit" />
+                </view>
+                <view class="generic-group-title-wrap">
+                  <view
+                    class="panel-title"
+                    data-group-id="{{group.id}}"
+                    catchlongpress="openStorageScalarRegisterEdit"
+                  >{{group.displayName || group.name}}</view>
+                  <view class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+                </view>
               </view>
-            </view>
-            <view wx:if="{{isModbusProtocol}}" class="panel-actions generic-group-actions">
+              <view class="generic-scalar-value">{{group.scalarValueText || '--'}}</view>
+            </block>
+            <block wx:else>
               <view
-                class="panel-action-button {{connectedDevice && !group.addressOverflow ? '' : 'is-disabled'}}"
+                class="panel-heading-toggle"
                 data-group-id="{{group.id}}"
-                bindtap="readParameterGroup"
+                bindtap="openParameterGroup"
               >
-                读取
+                <view class="panel-icon icon-terminal">
+                  <image class="panel-icon-image" src="/assets/icons/terminal-white.png" mode="aspectFit" />
+                </view>
+                <view class="generic-group-title-wrap">
+                  <view class="panel-title" data-group-id="{{group.id}}" catchlongpress="openParameterGroupEdit">{{group.displayName || group.name}}</view>
+                  <view class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+                </view>
               </view>
-              <view
-                wx:if="{{group.writable}}"
-                class="panel-action-button {{connectedDevice && !group.addressOverflow ? '' : 'is-disabled'}}"
-                data-group-id="{{group.id}}"
-                bindtap="writeParameterGroup"
-              >
-                写入
+              <view wx:if="{{isModbusProtocol}}" class="panel-actions generic-group-actions">
+                <view
+                  class="panel-action-button {{connectedDevice && !group.addressOverflow ? '' : 'is-disabled'}}"
+                  data-group-id="{{group.id}}"
+                  bindtap="readParameterGroup"
+                >
+                  读取
+                </view>
+                <view
+                  wx:if="{{group.writable}}"
+                  class="panel-action-button {{connectedDevice && !group.addressOverflow ? '' : 'is-disabled'}}"
+                  data-group-id="{{group.id}}"
+                  bindtap="writeParameterGroup"
+                >
+                  写入
+                </view>
+                <view wx:if="{{parameterCardControlEnabled}}" class="entry-chevron"></view>
               </view>
-              <view wx:if="{{parameterCardControlEnabled}}" class="entry-chevron"></view>
-            </view>
+            </block>
           </view>
           <view wx:if="{{!parameterCardControlEnabled && group.expanded}}" class="generic-group-inline-registers">
             <view

+ 42 - 7
pages/settings/settings.js

@@ -18,6 +18,21 @@ const {
   getWxApi
 } = require('../../utils/base-utils.js')
 
+function mergeDraftFields(data = {}, nextState = {}, draftFields = {}) {
+  const draftValues = {}
+
+  Object.keys(draftFields).forEach((field) => {
+    if (draftFields[field]) {
+      draftValues[field] = data[field]
+    }
+  })
+
+  return {
+    ...nextState,
+    ...draftValues
+  }
+}
+
 Page({
   data: {
     ...getSettingsPageState(),
@@ -30,31 +45,32 @@ Page({
   onLoad() {
     this.pageToast = createPageToast(this, this.data)
     this.crcFileBytes = null
+    this.settingsDraftFields = {}
     settingsService.init()
     themeService.init()
     bootloaderService.init()
 
     this.unsubscribeSettings = settingsService.subscribe((settingsState) => {
-      this.setData(getSettingsPageState(settingsState, themeService.getState()))
+      this.setData(this.mergeSettingsDrafts(getSettingsPageState(settingsState, themeService.getState())))
     })
     this.unsubscribeTheme = themeService.subscribe((themeState) => {
-      this.setData(getSettingsPageState(settingsService.getState(), themeState))
+      this.setData(this.mergeSettingsDrafts(getSettingsPageState(settingsService.getState(), themeState)))
     })
     this.unsubscribeTransport = transport.subscribe((transportState) => {
-      this.setData(getSettingsPageState(
+      this.setData(this.mergeSettingsDrafts(getSettingsPageState(
         settingsService.getState(),
         themeService.getState(),
         transportState,
         bootloaderService.getState()
-      ))
+      )))
     })
     this.unsubscribeBootloader = bootloaderService.subscribe((bootloaderState) => {
-      this.setData(getSettingsPageState(
+      this.setData(this.mergeSettingsDrafts(getSettingsPageState(
         settingsService.getState(),
         themeService.getState(),
         transport.getState(),
         bootloaderState
-      ))
+      )))
     })
   },
 
@@ -67,7 +83,7 @@ Page({
       this.pageToast.setActive(true)
     }
 
-    this.setData(getSettingsPageState())
+    this.setData(this.mergeSettingsDrafts(getSettingsPageState()))
   },
 
   onHide() {
@@ -121,12 +137,25 @@ Page({
       : ''
     if (!field) return
 
+    if (!this.settingsDraftFields) this.settingsDraftFields = {}
+    this.settingsDraftFields[field] = true
     this.setData({
       [field]: event.detail.value
     })
   },
 
+  mergeSettingsDrafts(nextState) {
+    return mergeDraftFields(this.data, nextState, this.settingsDraftFields)
+  },
+
+  clearSettingsDraft(field) {
+    if (this.settingsDraftFields && field) {
+      delete this.settingsDraftFields[field]
+    }
+  },
+
   onModbusSlaveAddressBlur(event) {
+    this.clearSettingsDraft('modbusSlaveAddress')
     settingsService.setModbusSlaveAddress(event.detail.value)
   },
 
@@ -141,11 +170,17 @@ Page({
     settingsService.setParameterAutoPollEnabled(!!event.detail.value)
   },
 
+  onStorageAccessDefaultEndianChange(event) {
+    settingsService.setStorageAccessDefaultEndian(event.detail.value ? 'big' : 'little')
+  },
+
   onParameterPollIntervalBlur(event) {
+    this.clearSettingsDraft('parameterPollInterval')
     settingsService.setParameterPollInterval(event.detail.value)
   },
 
   onParameterMaxPacketLengthBlur(event) {
+    this.clearSettingsDraft('parameterMaxPacketLength')
     settingsService.setParameterMaxPacketLength(event.detail.value)
   },
 

+ 14 - 0
pages/settings/settings.wxml

@@ -832,6 +832,20 @@
           <view class="settings-picker-value">{{protocolText}}</view>
         </picker>
       </view>
+      <view
+        wx:if="{{isStorageAccessProtocol}}"
+        class="settings-row"
+      >
+        <view class="settings-row-main">
+          <view class="param-name">默认大端模式</view>
+          <view class="param-meta">当前 {{storageAccessDefaultEndianText}}</view>
+        </view>
+        <switch
+          checked="{{storageAccessDefaultEndianBigSwitch}}"
+          color="#0f766e"
+          bindchange="onStorageAccessDefaultEndianChange"
+        />
+      </view>
       <view
         wx:if="{{isStorageAccessProtocol}}"
         class="settings-row settings-tool-row"

+ 131 - 31
protocols/storage-access/index.js

@@ -3,12 +3,17 @@ const {
 } = require('../../utils/base-utils.js')
 const {
   bytesToHex,
+  bytesToWords,
   bytesToWordsLE,
   formatHexNumber,
   readByte,
+  readUint16BE,
   readUint16LE,
+  readUint32BE,
   readUint32LE,
+  splitUint16BE,
   splitUint16LE,
+  splitUint32BE,
   splitUint32LE,
   toByteArray
 } = require('../../utils/binary-utils.js')
@@ -112,10 +117,47 @@ const VALID_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDA
 const MEMORY_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
 const WRITABLE_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA]
 const RESERVED_AREAS = [0x05, 0x06]
-const readWord = readUint16LE
-const readDword = readUint32LE
-const splitWord = splitUint16LE
-const splitDword = splitUint32LE
+const readCrcWord = readUint16LE
+
+function normalizeMemoryEndian(value, fallback = MEMORY_ENDIAN.LITTLE) {
+  const text = String(value || '').trim().toLowerCase()
+  if (text === MEMORY_ENDIAN.LITTLE || text === 'le' || text === '1') return MEMORY_ENDIAN.LITTLE
+  if (text === MEMORY_ENDIAN.BIG || text === 'be' || text === '0') return MEMORY_ENDIAN.BIG
+
+  return fallback
+}
+
+function isLittleMemoryEndian(memoryEndian) {
+  return normalizeMemoryEndian(memoryEndian) === MEMORY_ENDIAN.LITTLE
+}
+
+function readWord(bytes, offset, memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  return isLittleMemoryEndian(memoryEndian)
+    ? readUint16LE(bytes, offset)
+    : readUint16BE(bytes, offset)
+}
+
+function readDword(bytes, offset, memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  return isLittleMemoryEndian(memoryEndian)
+    ? readUint32LE(bytes, offset)
+    : readUint32BE(bytes, offset)
+}
+
+function splitWord(value, memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  return isLittleMemoryEndian(memoryEndian)
+    ? splitUint16LE(value)
+    : splitUint16BE(value)
+}
+
+function splitDword(value, memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  return isLittleMemoryEndian(memoryEndian)
+    ? splitUint32LE(value)
+    : splitUint32BE(value)
+}
+
+function bytesToProtocolWords(bytes, memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  return isLittleMemoryEndian(memoryEndian) ? bytesToWordsLE(bytes) : bytesToWords(bytes)
+}
 
 function toByte(value, label) {
   if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
@@ -266,6 +308,16 @@ function getPayloadLimitFromFrame(maxFrameBytes, overhead) {
   return Math.max(0, Math.min(MAX_PAYLOAD_BYTES, frameBytes - overhead))
 }
 
+function getRequiredPayloadLimit(maxFrameBytes, overhead, actionText) {
+  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
+  const payloadLimit = getPayloadLimitFromFrame(frameBytes, overhead)
+  if (frameBytes !== UNLIMITED_FRAME_BYTES && payloadLimit <= 0) {
+    throw new Error(`最大包长至少需要 ${overhead + 1} 字节才能${actionText}`)
+  }
+
+  return payloadLimit
+}
+
 function getMaxReadByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, area = AREA.ADDR32) {
   return getPayloadLimitFromFrame(maxFrameBytes, getReadResponseOverhead(area))
 }
@@ -325,7 +377,7 @@ function hasValidStorageCrc(bytes) {
   if (frame.length >= 4) return hasValidCrc16Ccitt(frame, STORAGE_CRC_OPTIONS)
 
   const expected = crc16Ccitt(frame.slice(0, -2), STORAGE_CRC_OPTIONS)
-  const received = readWord(frame, frame.length - 2)
+  const received = readCrcWord(frame, frame.length - 2)
   return expected === received
 }
 
@@ -339,15 +391,22 @@ function buildReadFrame(area, address, byteLength, options = {}) {
     return buildCodeInfoDescriptorFrame()
   }
 
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
   const addressBytes = getAddressFieldByteLength(normalizedArea)
   const startAddress = addressBytes === ADDRESS32_BYTE_LENGTH
     ? toUint32(Number(address), '内存地址')
     : toWord(Number(address), '内存地址')
-  const maxByteLength = getMaxReadByteLength(options.maxFrameBytes, normalizedArea)
+  const maxByteLength = getRequiredPayloadLimit(
+    options.maxFrameBytes,
+    getReadResponseOverhead(normalizedArea),
+    '读取 1 字节'
+  )
   const length = toByteLength(Number(byteLength), '读取字节长度', maxByteLength || MAX_PAYLOAD_BYTES)
   const command = buildCommand(normalizedArea, false)
-  const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH ? splitDword(startAddress) : splitWord(startAddress)
-  const lengthParts = splitWord(toWord(length, '读取字节长度'))
+  const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH
+    ? splitDword(startAddress, memoryEndian)
+    : splitWord(startAddress, memoryEndian)
+  const lengthParts = splitWord(toWord(length, '读取字节长度'), memoryEndian)
 
   return appendStorageCrc([command].concat(addressParts, lengthParts))
 }
@@ -358,16 +417,23 @@ function buildWriteFrame(area, address, bytes, options = {}) {
     throw new Error(`${AREA_NAMES[normalizedArea] || '该'} 区不可写`)
   }
 
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
   const addressBytes = getAddressFieldByteLength(normalizedArea)
   const startAddress = addressBytes === ADDRESS32_BYTE_LENGTH
     ? toUint32(Number(address), '内存地址')
     : toWord(Number(address), '内存地址')
   const dataBytes = toByteArray(bytes).map((byte) => toByte(Number(byte), '写入字节'))
-  const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes, normalizedArea)
+  const maxByteLength = getRequiredPayloadLimit(
+    options.maxFrameBytes,
+    getWriteRequestOverhead(normalizedArea),
+    '写入 1 字节'
+  )
   const length = toByteLength(dataBytes.length, '写入字节长度', maxByteLength || MAX_PAYLOAD_BYTES)
   const command = buildCommand(normalizedArea, true)
-  const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH ? splitDword(startAddress) : splitWord(startAddress)
-  const lengthParts = splitWord(toWord(length, '写入字节长度'))
+  const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH
+    ? splitDword(startAddress, memoryEndian)
+    : splitWord(startAddress, memoryEndian)
+  const lengthParts = splitWord(toWord(length, '写入字节长度'), memoryEndian)
 
   return appendStorageCrc([command].concat(addressParts, lengthParts, dataBytes))
 }
@@ -392,13 +458,14 @@ function parseCodeInfoDescriptorBytes(bytes) {
   if (!endianMark.memoryEndian) {
     throw new Error('CodeInfo 描述符字节序标记无效')
   }
+  const memoryEndian = endianMark.memoryEndian
 
   return {
-    codeInfoAddress: readDword(dataBytes, 0),
+    codeInfoAddress: readDword(dataBytes, 0, memoryEndian),
     codeInfoAddressWidth: dataBytes[6] & 0xFF,
-    codeInfoByteLength: readWord(dataBytes, 4),
+    codeInfoByteLength: readWord(dataBytes, 4, memoryEndian),
     codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH),
-    codeInfoMaxPacketLength: readWord(dataBytes, 7),
+    codeInfoMaxPacketLength: readWord(dataBytes, 7, memoryEndian),
     codeInfoMemoryEndian: endianMark.memoryEndian,
     codeInfoMemoryEndianMark: endianMark.marker
   }
@@ -408,13 +475,14 @@ function formatHex(bytes) {
   return bytesToHex(bytes, ' ')
 }
 
-function parseStorageAccessResponse(bytes) {
+function parseStorageAccessResponse(bytes, options = {}) {
   const frame = toByteArray(bytes)
   if (frame.length < 3 || !hasValidStorageCrc(frame)) return null
 
   const command = frame[0] & 0xFF
   const decoded = decodeCommand(command)
   if (decoded.isControl) return parseStorageControlResponse(frame, decoded)
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
 
   if (decoded.hasError) {
     if (frame.length !== EXCEPTION_RESPONSE_LENGTH) return null
@@ -438,6 +506,7 @@ function parseStorageAccessResponse(bytes) {
     if (decoded.isWrite || frame.length !== CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH) return null
 
     const dataBytes = frame.slice(1, 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH)
+    const descriptor = parseCodeInfoDescriptorBytes(dataBytes)
     const response = {
       address: CODE_INFO_DESCRIPTOR_ADDRESS,
       addressWidth: 0,
@@ -450,12 +519,15 @@ function parseStorageAccessResponse(bytes) {
       isAddress32: false,
       isWrite: false,
       protocol: PROTOCOL_NAME,
-      words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
+      words: bytesToProtocolWords(
+        dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0),
+        descriptor.codeInfoMemoryEndian
+      )
     }
 
     return {
       ...response,
-      ...parseCodeInfoDescriptorBytes(dataBytes)
+      ...descriptor
     }
   }
 
@@ -466,11 +538,11 @@ function parseStorageAccessResponse(bytes) {
     if (frame.length !== getWriteResponseLength(decoded.area)) return null
 
     return {
-      address: decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1),
+      address: decoded.isAddress32 ? readDword(frame, 1, memoryEndian) : readWord(frame, 1, memoryEndian),
       addressWidth: decoded.isAddress32 ? 32 : 16,
       area: decoded.area,
       areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
-      byteLength: readWord(frame, 1 + addressBytes),
+      byteLength: readWord(frame, 1 + addressBytes, memoryEndian),
       command,
       dataBytes: [],
       isException: false,
@@ -482,8 +554,8 @@ function parseStorageAccessResponse(bytes) {
 
   if (frame.length < getReadResponseOverhead(decoded.area)) return null
 
-  const address = decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1)
-  const byteLength = readWord(frame, 1 + addressBytes)
+  const address = decoded.isAddress32 ? readDword(frame, 1, memoryEndian) : readWord(frame, 1, memoryEndian)
+  const byteLength = readWord(frame, 1 + addressBytes, memoryEndian)
   const dataStart = headerLength
   const dataEnd = dataStart + byteLength
   if (frame.length !== dataEnd + 2) return null
@@ -502,7 +574,7 @@ function parseStorageAccessResponse(bytes) {
     isAddress32: decoded.isAddress32,
     isWrite: false,
     protocol: PROTOCOL_NAME,
-    words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
+    words: bytesToProtocolWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0), memoryEndian)
   }
 
   return response
@@ -550,7 +622,7 @@ function parseStorageControlResponse(frame, decoded) {
   }
 }
 
-function parseStorageAccessRequest(bytes) {
+function parseStorageAccessRequest(bytes, options = {}) {
   const frame = toByteArray(bytes)
   if (frame.length < 3 || !hasValidStorageCrc(frame)) return null
 
@@ -582,12 +654,13 @@ function parseStorageAccessRequest(bytes) {
   }
   if (frame.length < READ_REQUEST_LENGTH_16) return null
 
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
   const addressBytes = decoded.addressBytes
   const headerLength = getMemoryHeaderLength(decoded.area)
   if (frame.length < headerLength + 2) return null
 
-  const address = decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1)
-  const byteLength = readWord(frame, 1 + addressBytes)
+  const address = decoded.isAddress32 ? readDword(frame, 1, memoryEndian) : readWord(frame, 1, memoryEndian)
+  const byteLength = readWord(frame, 1 + addressBytes, memoryEndian)
   const expectedLength = decoded.isWrite
     ? headerLength + byteLength + 2
     : headerLength + 2
@@ -655,7 +728,11 @@ function getExpectedResponseLength(expected, responseCommand, responseBytes = []
   const headerLength = getMemoryHeaderLength(expected.area)
   if (responseBytes.length < headerLength) return 0
 
-  return headerLength + readWord(responseBytes, 1 + getAddressFieldByteLength(expected.area)) + 2
+  return headerLength + readWord(
+    responseBytes,
+    1 + getAddressFieldByteLength(expected.area),
+    expected.memoryEndian
+  ) + 2
 }
 
 function isExpectedResponse(response, expected) {
@@ -760,7 +837,7 @@ function readResponseFromBuffer(buffer, expected, options = {}) {
     }
 
     const frameBytes = buffer.slice(0, responseLength)
-    const response = parseStorageAccessResponse(frameBytes)
+    const response = parseStorageAccessResponse(frameBytes, expected)
     if (!response) {
       return {
         frameBytes,
@@ -807,6 +884,7 @@ function createExpected(area, address, byteLength, isWrite, kind, options = {})
     isAddress32: isAddress32Area(normalizedArea),
     isWrite,
     kind,
+    memoryEndian: normalizeMemoryEndian(options.memoryEndian),
     operation: isWrite ? 'write' : 'read',
     protocol: PROTOCOL_NAME,
     quantity: byteLength
@@ -861,15 +939,36 @@ function splitQuantity(startAddress, quantity, maxQuantity) {
 }
 
 function getReadChunks(startAddress, byteLength, options = {}) {
-  const maxByteLength = getMaxReadByteLength(options.maxFrameBytes, options.area)
+  const normalizedArea = normalizeMemoryArea(options.area === undefined ? AREA.ADDR32 : options.area)
+  if (isCodeInfoDescriptorArea(normalizedArea)) {
+    const frameLimit = normalizeMaxFrameBytes(options.maxFrameBytes)
+    if (frameLimit !== UNLIMITED_FRAME_BYTES && frameLimit < CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH) {
+      throw new Error(`最大包长至少需要 ${CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH} 字节才能读取 CodeInfo 描述符`)
+    }
+
+    return [{
+      address: CODE_INFO_DESCRIPTOR_ADDRESS,
+      quantity: CODE_INFO_DESCRIPTOR_BYTE_LENGTH
+    }]
+  }
+  const maxByteLength = getRequiredPayloadLimit(
+    options.maxFrameBytes,
+    getReadResponseOverhead(normalizedArea),
+    '读取 1 字节'
+  )
 
-  return splitQuantity(startAddress, byteLength, maxByteLength || byteLength)
+  return splitQuantity(startAddress, byteLength, maxByteLength)
 }
 
 function getWriteChunks(startAddress, bytes, options = {}) {
   const sourceBytes = Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
-  const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes, options.area)
-  const chunks = splitQuantity(startAddress, sourceBytes.length, maxByteLength || sourceBytes.length)
+  const normalizedArea = normalizeMemoryArea(options.area === undefined ? AREA.ADDR32 : options.area)
+  const maxByteLength = getRequiredPayloadLimit(
+    options.maxFrameBytes,
+    getWriteRequestOverhead(normalizedArea),
+    '写入 1 字节'
+  )
+  const chunks = splitQuantity(startAddress, sourceBytes.length, maxByteLength)
   let offset = 0
 
   return chunks.map((chunk) => {
@@ -951,6 +1050,7 @@ module.exports = {
   normalizeDescriptorAddressWidth,
   normalizeArea,
   normalizeMaxFrameBytes,
+  normalizeMemoryEndian,
   normalizeMemoryArea,
   parseCodeInfoDescriptorBytes,
   resolveDescriptorMaxFrameBytes,

+ 22 - 3
store/settings-store.js

@@ -26,11 +26,12 @@ const DEFAULT_SETTINGS = {
   parameterCardControlEnabled: true,
   parameterMaxPacketLength: 64,
   parameterPollInterval: 100,
-  protocolMode: DEFAULT_PROTOCOL_MODE
+  protocolMode: DEFAULT_PROTOCOL_MODE,
+  storageAccessDefaultEndian: 'little'
 }
 const STATUS_POLL_MIN_INTERVAL = 100
 const STATUS_POLL_MAX_INTERVAL = 3000
-const PARAMETER_MIN_PACKET_LENGTH = 32
+const PARAMETER_MIN_PACKET_LENGTH = 14
 
 const state = {
   ...DEFAULT_SETTINGS
@@ -49,6 +50,14 @@ function normalizeParameterPacketLength(value, fallback = DEFAULT_SETTINGS.param
   return Math.max(rounded, PARAMETER_MIN_PACKET_LENGTH)
 }
 
+function normalizeStorageAccessEndian(value, fallback = DEFAULT_SETTINGS.storageAccessDefaultEndian) {
+  const text = String(value || '').trim().toLowerCase()
+  if (text === 'big' || text === 'be' || text === '1') return 'big'
+  if (text === 'little' || text === 'le' || text === '0') return 'little'
+
+  return fallback
+}
+
 function isModbusProtocol(value = state.protocolMode) {
   return isModbusProtocolMode(value)
 }
@@ -83,7 +92,8 @@ function normalizeSettings(settings = {}) {
     parameterCardControlEnabled: settings.parameterCardControlEnabled !== false,
     parameterMaxPacketLength,
     parameterPollInterval,
-    protocolMode
+    protocolMode,
+    storageAccessDefaultEndian: normalizeStorageAccessEndian(settings.storageAccessDefaultEndian)
   }
 }
 
@@ -215,6 +225,13 @@ function setParameterPollInterval(value) {
   })
 }
 
+function setStorageAccessDefaultEndian(value) {
+  init()
+  setState({
+    storageAccessDefaultEndian: normalizeStorageAccessEndian(value, state.storageAccessDefaultEndian)
+  })
+}
+
 module.exports = {
   PARAMETER_MIN_PACKET_LENGTH,
   PROTOCOL_MODE,
@@ -234,7 +251,9 @@ module.exports = {
   setParameterCardControlEnabled,
   setParameterMaxPacketLength,
   setParameterPollInterval,
+  setStorageAccessDefaultEndian,
   setProtocolMode,
   normalizeProtocolMode,
+  normalizeStorageAccessEndian,
   subscribe
 }

+ 44 - 0
utils/binary-utils.js

@@ -128,6 +128,49 @@ function bytesToUtf8Text(bytes = []) {
   }
 }
 
+function decodeBytesWithTextDecoder(bytes = [], encoding) {
+  if (typeof TextDecoder === 'undefined') return null
+
+  try {
+    const decoder = new TextDecoder(encoding, { fatal: true })
+
+    return decoder.decode(new Uint8Array(bytes))
+  } catch (error) {
+    return null
+  }
+}
+
+function decodeUtf8Strict(bytes = []) {
+  if (!bytes.length) return ''
+
+  let encoded = ''
+
+  bytes.forEach((byte) => {
+    encoded += `%${(byte & 0xFF).toString(16).padStart(2, '0').toUpperCase()}`
+  })
+
+  try {
+    return decodeURIComponent(encoded)
+  } catch (error) {
+    return null
+  }
+}
+
+function bytesToAutoText(bytes = []) {
+  const trimmed = trimTrailingNullBytes(bytes)
+  if (!trimmed.length) return ''
+
+  const utf8Text = decodeBytesWithTextDecoder(trimmed, 'utf-8')
+    || decodeUtf8Strict(trimmed)
+  if (utf8Text !== null) return utf8Text
+
+  const gbkText = decodeBytesWithTextDecoder(trimmed, 'gbk')
+    || decodeBytesWithTextDecoder(trimmed, 'gb18030')
+  if (gbkText !== null) return gbkText
+
+  return bytesToAsciiText(trimmed)
+}
+
 function formatBytes(byteLength) {
   const length = Number(byteLength) || 0
   if (length >= 1024 && length % 1024 === 0) return `${length / 1024} KB`
@@ -216,6 +259,7 @@ function wordsToBytesLE(words = [], byteLength = words.length * 2) {
 }
 
 module.exports = {
+  bytesToAutoText,
   bytesToBase64,
   bytesToBin,
   bytesToHex,

+ 11 - 11
协议架构说明.md

@@ -104,8 +104,8 @@ BLE 透传链路
 - `CMD bit3` 为普通读写位,`bit4/bit5` 保留,`bit6` 为特殊指令位,`bit7` 为回帧故障标志。
 - 普通区域包括 `CODEINFO`、`DATA`、`IDATA`、`XDATA`、`CODE`、`ADDR32`,其中 `CODEINFO` 和 `CODE` 只读。
 - 特殊指令使用 `CMD=0x40 | special_code`,当前仅定义 `special_code=0x01` 复位。
-- CodeInfo 同步先发送 `00 + CRC` 读取 `area=0x00 CODEINFO`,获取小端序 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16` 和原始两字节 `ENDIAN_MARK16`,再按 `ADDR_WIDTH` 用 CODE 或 ADDR32 读取完整信息块。
-- 协议控制字段始终固定小端;结构体字段和单独变量等目标内存多字节值根据 `ENDIAN_MARK` 自动按大端或小端编解码,未同步时默认小端
+- CodeInfo 同步先发送 `00 + CRC` 读取 `area=0x00 CODEINFO`,获取 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16` 和原始两字节 `ENDIAN_MARK16`,再按 `ENDIAN_MARK` 与 `ADDR_WIDTH` 用 CODE 或 ADDR32 读取完整信息块。
+- CRC 固定低字节在前;普通帧 `ADDR/LEN`、描述符 `TLV_ADDR/TLV_LEN/MAX_PACKET`、CodeInfo 内存入口的 `byte_addr/byte_len/elem_byte_len/elem_count/dim_len`、结构体字段和基础变量等目标内存多字节值根据 `ENDIAN_MARK` 自动按大端或小端编解码,未同步时按设置页“默认大端模式”开关处理
 - 不直接调用 BLE 发送,不负责弹窗;普通内存协议 IO 由 `features/storage-access/protocol-io.js` 编排。
 
 完整帧格式和从机实现参考见 `存储访问协议.md`。
@@ -146,13 +146,13 @@ BLE 透传链路
 | `model.js` | 参数组/寄存器规范化、地址布局、显示文案、导入克隆和领域 API |
 | `register-io.js` | 读缓存解码、写入编码、组级字节/字编码和分片辅助 |
 | `value-types.js` | 数据类型元数据、字节/字长度、bit field 长度、类型分类 |
-| `value-number.js` | 整数、HEX、float 的解析、范围校验和格式化 |
-| `value-text.js` | ASCII/UTF-8 文本字段的编码和解码 |
+| `value-number.js` | 整数、HEX、float、double 和大宽度整数的解析、范围校验和格式化 |
+| `value-text.js` | 文本字段编码,以及 UTF-8/GBK/ASCII 顺序自动解码 |
 | `value-codec.js` | 寄存器值编解码、显示值和输入校验 |
 | `value-formula.js` | 读回标幺值到实际值的转换公式求值 |
 | `struct-parser.js` | C 结构体与 enum 定义解析入口 |
 | `struct-c-syntax.js` | C 注释剥离、typedef、struct、enum 和 declarator 解析 |
-| `struct-layout.js` | 结构体字段布局、数组展开、ASCII 字段、enum 映射和 bit field 展开 |
+| `struct-layout.js` | 结构体字段布局、数组展开、文本字段、enum 映射和 bit field 展开 |
 
 ### 5.2 CodeInfo 解析
 
@@ -161,11 +161,11 @@ BLE 透传链路
 职责:
 
 - 解析 CodeInfo 纯 TLV 信息块,未知 TLV 类型跳过。
-- 使用 CODEINFO 描述符返回的 `TLV_LEN` 决定 CodeInfo 总长度,并使用 `ADDR_WIDTH/MAX_PACKET` 作为同步上下文;内存入口地址宽度和区域由 TLV `TYPE` 自描述
-- 解析 UTF-8/ASCII 电机型号、芯片型号和转换相关可选 TLV 参数。
-- 固定 TLV `0x01~0x08` 映射真实存储区域、地址宽度和结构体/变量类型,并按结构体、单独变量成对排列;VALUE 为小端序 `addr(2/4) + byte_len16 + name_len8 + name`
-- `0x20~0x3F` 为自定义 TLV,板卡参数从 `0x40` 开始递增
-- 生成参数组初始结构,结构体未导入定义时按字节占位,单独变量按 TLV `byte_len` 显示为未配置原始字节,后续由 UI 或 enum 导入确定同长度的解释类型
+- 使用 CODEINFO 描述符返回的 `TLV_LEN` 决定 CodeInfo 总长度,并使用 `ADDR_WIDTH/MAX_PACKET` 作为同步上下文;内存入口地址宽度由描述符 `ADDR_WIDTH` 决定,16 位入口在 VALUE 中携带 `area8`,32 位入口固定为 `ADDR32`
+- 解析电机型号、芯片型号和转换相关可选 TLV 参数,文本按 UTF-8、GBK、ASCII 顺序自动解码
+- TYPE 直接定义基础变量、基础数组、enum 或结构体入口;VALUE 使用 `ENTRY_PREFIX + 名称/数组维度` 描述,`byte_addr/byte_len/elem_byte_len/elem_count/dim_len` 按 `ENDIAN_MARK` 解析
+- 内置入口包括 `0x01..0x0F` 基础变量、`0x10..0x1F` 基础数组、`0x20` enum 变量、`0x21` 结构体实例、`0x22` enum 数组、`0x23` 结构体数组;板卡参数从 `0x40` 开始递增,其他未定义 TYPE 作为项目扩展保留原始项并跳过业务解析
+- 生成参数组初始结构,基础变量按 TYPE 解码,结构体未导入定义时按字节占位,enum 按 `def_name` 匹配 typedef enum,数组按 `dim_len[]` 展开;结构体数组导入 typedef struct 后展开为 `var[i].field`、`var[i][j].field` 等
 
 ## 6. 功能服务
 
@@ -278,7 +278,7 @@ CodeInfo 读取只从同步流程进入:先读取 `area=0x00 CODEINFO` 描述
   -> features/storage-access/code-info-sync.readCodeInfoBlock
   -> features/storage-access/protocol-io.readMemory
   -> protocols/storage-access/index.js 构建普通读帧
-  -> 发送 00 + CRC 读取 area=0x00 CODEINFO 描述符,获得小端序 TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16 和原始 ENDIAN_MARK16
+  -> 发送 00 + CRC 读取 area=0x00 CODEINFO 描述符,获得 TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16 和原始 ENDIAN_MARK16
   -> 按描述符和设置页最大包长分片读取 CodeInfo TLV 信息块
   -> domain/storage-access/code-info-parser.parseCodeInfo
   -> domain/storage-access/code-info-parser.createGroupsFromCodeInfo

+ 219 - 89
存储访问协议.md

@@ -2,23 +2,20 @@
 
 ## 1. 协议定位
 
-存储访问协议用于上位机通过蓝牙透传链路按字节访问从机 MCU 的存储区域。协议为单主单从模型,不带从机地址字段,不属于标准 Modbus RTU。
+存储访问协议用于上位机通过蓝牙透传链路按字节访问从机 MCU 的存储区域。
 
-小程序中该协议与标准 Modbus RTU、Bootloader 升级协议平级:
-
-- 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
 - 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
 - 复位使用 `bit6=1` 的特殊指令帧;当前仅定义复位特殊指令。
 - CodeInfo 同步先读取普通区域 `area=0x00` 的 codeinfo 描述符,再按描述符地址位宽读取 TLV 信息块。
 
 ## 2. 字节序与 CRC
 
-除 CRC 算法内部计算外,所有多字节协议控制字段均为小端序。目标内存变量的多字节值在 CodeInfo 同步后按 `ENDIAN_MARK` 自动选择大端或小端;未同步或未声明时默认小端
+CRC 输出固定为低字节在前。除 CRC 外,协议中的地址、长度和多字节数据字段都按 CodeInfo 描述符里的 `ENDIAN_MARK` 自动选择大端或小端;未同步或未声明时按设置页“默认大端模式”开关处理
 
 ```text
-32 位地址 : ADDR_0 ADDR_1 ADDR_2 ADDR_3
-16 位长度 : LEN_L LEN_H
-CRC 输出  : CRC_L CRC_H
+ENDIAN_MARK = 55 AA: 32 位地址 ADDR_3 ADDR_2 ADDR_1 ADDR_0,16 位长度 LEN_H LEN_L
+ENDIAN_MARK = AA 55: 32 位地址 ADDR_0 ADDR_1 ADDR_2 ADDR_3,16 位长度 LEN_L LEN_H
+CRC 输出固定        : CRC_L CRC_H
 ```
 
 CRC 使用 `CRC16-CCITT-FALSE`:
@@ -59,8 +56,8 @@ bit2~bit0 AREA      普通读写区域码
 普通内存命令生成规则:
 
 ```text
-读请求   : CMD = MODE
-写请求   : CMD = 0x08 | MODE
+读请求   : CMD = AREA
+写请求   : CMD = 0x08 | AREA
 异常响应 : CMD_ERR = CMD | 0x80
 ```
 
@@ -92,17 +89,19 @@ bit2~bit0 AREA      普通读写区域码
 ### 5.1 读请求
 
 ```text
-AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
-AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
+AREA=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_L CRC_H
 ```
 
+上方帧格式以 `ENDIAN_MARK=55 AA` 的大端设备为例;如果 `ENDIAN_MARK=AA 55`,`ADDR` 和 `LEN` 字节顺序反转,CRC 仍为 `CRC_L CRC_H`。
+
 长度分别为 9 字节或 7 字节。
 
 ### 5.2 写请求
 
 ```text
-AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H DATA... CRC_L CRC_H
-AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
+AREA=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_L CRC_H
 ```
 
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
@@ -110,8 +109,8 @@ AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 ### 5.3 正常读响应
 
 ```text
-AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H DATA... CRC_L CRC_H
-AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
+AREA=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_L CRC_H
 ```
 
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
@@ -121,8 +120,8 @@ AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 ### 5.4 正常写响应
 
 ```text
-AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
-AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
+AREA=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_L CRC_H
 ```
 
 长度分别为 9 字节或 7 字节。
@@ -188,7 +187,7 @@ CodeInfo 同步分两步:
 从机成功响应:
 
 ```text
-00 TLV_ADDR_0 TLV_ADDR_1 TLV_ADDR_2 TLV_ADDR_3 TLV_LEN_L TLV_LEN_H ADDR_WIDTH MAX_PACKET_L MAX_PACKET_H ENDIAN_MARK_0 ENDIAN_MARK_1 CRC_L CRC_H
+00 TLV_ADDR(4B) TLV_LEN(2B) ADDR_WIDTH MAX_PACKET(2B) ENDIAN_MARK_0 ENDIAN_MARK_1 CRC_L CRC_H
 ```
 
 响应 DATA 字段:
@@ -201,7 +200,7 @@ CodeInfo 同步分两步:
 | `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度,包含 CMD/ADDR/LEN/DATA/CRC;为 0 表示未声明 |
 | `ENDIAN_MARK` | 2 | 目标内存值字节序标记;按字节读取为 `55 AA` 表示大端,`AA 55` 表示小端 |
 
-描述符区域 `CODEINFO` 只读,不支持写入。`CMD`、`ADDR`、普通读写 `LEN`、`TLV_LEN`、`MAX_PACKET`、固定内存入口 `byte_addr/byte_len` 等多字节协议控制字段始终按小端序解析;TLV `LEN` 与 `name_len` 为单字节字段,不涉及大小端。`ENDIAN_MARK` 不作为协议控制数值转换,而作为原始两字节同步标记:按字节读取为 `55 AA` 表示目标内存多字节值大端,`AA 55` 表示目标内存多字节值小端。
+描述符区域 `CODEINFO` 只读,不支持写入。`ENDIAN_MARK` 按原始两字节同步标记读取:`55 AA` 表示大端,`AA 55` 表示小端。上位机先识别该标记,再按标记解析描述符里的 `TLV_ADDR/TLV_LEN/MAX_PACKET`,以及后续普通读写帧的 `ADDR/LEN`、CodeInfo 内存入口中的 `byte_addr/byte_len/elem_byte_len/elem_count/dim_len` 和目标内存多字节数据。TLV `TYPE`、`LEN`、`area`、`name_len`、`def_name_len`、`var_name_len`、`dim_count` 为单字节字段,不涉及大小端。
 
 ### 8.2 读取完整 CodeInfo
 
@@ -241,48 +240,144 @@ TYPE LEN VALUE...
 
 上位机解析到未知 `TYPE` 时跳过该项。任何 TLV 项声明长度超过 `TLV_LEN` 剩余字节时,整段 CodeInfo 视为格式错误。`TLV_LEN` 由描述符使用 16 位字段给出,因此整段 CodeInfo 可以超过 255 字节,只是单个 TLV 项不能超过 255 字节。
 
-### 9.1 固定内存入口 TLV
+### 9.1 内存入口 TLV
 
-结构体实例和单独变量也是 TLV 项,不再额外保存 `entry_kind` 字段:
-
-| TYPE | 名称 | 地址宽度 | 小程序处理 |
-|---:|---|---:|---|
-| `0x01` | DATA 结构体实例 | 16 位 | 创建结构体组;未导入定义时按 `00`、`01`、`02`... 字节占位 |
-| `0x02` | DATA 单独变量 | 16 位 | 创建单变量组;`byte_len` 只定义长度,类型需在 UI 中配置为有符号/无符号整数、`float`,或导入 enum 后按枚举项显示 |
-| `0x03` | IDATA 结构体实例 | 16 位 | 创建结构体组;未导入定义时按 `00`、`01`、`02`... 字节占位 |
-| `0x04` | IDATA 单独变量 | 16 位 | 创建单变量组;类型由 UI/enum 配置,长度来自 `byte_len` |
-| `0x05` | XDATA 结构体实例 | 16 位 | 创建结构体组 |
-| `0x06` | XDATA 单独变量 | 16 位 | 创建单变量组;类型由 UI/enum 配置,长度来自 `byte_len` |
-| `0x07` | ADDR32 结构体实例 | 32 位 | 创建结构体组,统一 32 位字节地址 |
-| `0x08` | ADDR32 单独变量 | 32 位 | 创建单变量组,统一 32 位字节地址;类型由 UI/enum 配置,长度来自 `byte_len` |
+CodeInfo 内存入口的地址宽度由 CODEINFO 描述符中的 `ADDR_WIDTH` 决定,不再由 TLV `TYPE` 决定。
 
-16 位地址入口 `VALUE`
+当 `ADDR_WIDTH=16` 时,所有内存入口 `VALUE` 以前缀 `area8 + byte_addr16 + byte_len16` 开始:
 
 | 字段 | 长度 | 说明 |
 |---|---:|---|
-| `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址,小端序 |
-| `byte_len` | 2 | 结构体实例或单独变量的字节长度,小端序 |
-| `name_len` | 1 | `name` 字节长度 |
-| `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
+| `area` | 1 | 目标存储区域:`0x01 DATA`、`0x02 IDATA`、`0x03 XDATA`、`0x04 CODE` |
+| `byte_addr` | 2 | 区域内字节地址,按 `ENDIAN_MARK` 解析 |
+| `byte_len` | 2 | 变量、结构体或数组总字节数,按 `ENDIAN_MARK` 解析 |
 
-因此 16 位地址固定内存入口 TLV 的 `LEN = 0x05 + name_len`,受 TLV 单项长度限制,`name_len` 最大为 250。
-
-32 位地址入口 `VALUE`:
+当 `ADDR_WIDTH=32` 时,所有内存入口 `VALUE` 以前缀 `byte_addr32 + byte_len16` 开始,目标区域固定为 `ADDR32`,不再携带 `area` 字段。
 
 | 字段 | 长度 | 说明 |
 |---|---:|---|
-| `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址,小端序 |
-| `byte_len` | 2 | 结构体实例或单独变量的字节长度,小端序 |
-| `name_len` | 1 | `name` 字节长度 |
-| `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
+| `byte_addr` | 4 | 统一 32 位字节地址,按 `ENDIAN_MARK` 解析 |
+| `byte_len` | 2 | 变量、结构体或数组总字节数,按 `ENDIAN_MARK` 解析 |
+
+下面用 `ENTRY_PREFIX` 表示上述二选一前缀。16 位地址前缀长度为 5 字节,32 位地址前缀长度为 6 字节。
+
+TYPE 直接定义入口形态和基础数据类型,不再额外携带元素类型字段。未在下表定义的 TYPE 均可由项目自定义;通用上位机解析器只保留原始 TLV 项并跳过业务解析。
+
+基础变量:
+
+| TYPE | 名称 | 字节数 | VALUE 格式 |
+|---:|---|---:|---|
+| `0x01` | raw 变量 | `byte_len` 决定 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x02` | int8_t 变量 | 1 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x03` | uint8_t 变量 | 1 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x04` | int16_t 变量 | 2 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x05` | uint16_t 变量 | 2 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x06` | int32_t 变量 | 4 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x07` | uint32_t 变量 | 4 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x08` | float32 变量 | 4 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x09` | double 变量 | 8 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0A` | int64_t 变量 | 8 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0B` | uint64_t 变量 | 8 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0C` | int128_t 变量 | 16 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0D` | uint128_t 变量 | 16 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0E` | int256_t 变量 | 32 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x0F` | uint256_t 变量 | 32 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+
+数组变量:
+
+```text
+ARRAY_VALUE =
+ENTRY_PREFIX
+elem_byte_len16
+elem_count16
+dim_count8
+dim_len16[dim_count]
+var_name_len8
+var_name
+```
+
+| TYPE | 名称 | 元素字节数 | 说明 |
+|---:|---|---:|---|
+| `0x10` | raw 数组 | `elem_byte_len` 决定 | 原始字节数组 |
+| `0x11` | int8_t 数组 | 1 | 有符号 8 位整数数组 |
+| `0x12` | uint8_t 数组 | 1 | 无符号 8 位整数数组 |
+| `0x13` | int16_t 数组 | 2 | 有符号 16 位整数数组 |
+| `0x14` | uint16_t 数组 | 2 | 无符号 16 位整数数组 |
+| `0x15` | int32_t 数组 | 4 | 有符号 32 位整数数组 |
+| `0x16` | uint32_t 数组 | 4 | 无符号 32 位整数数组 |
+| `0x17` | float32 数组 | 4 | IEEE-754 float 数组 |
+| `0x18` | double 数组 | 8 | IEEE-754 double 数组 |
+| `0x19` | int64_t 数组 | 8 | 有符号 64 位整数数组 |
+| `0x1A` | uint64_t 数组 | 8 | 无符号 64 位整数数组 |
+| `0x1B` | int128_t 数组 | 16 | 有符号 128 位整数数组 |
+| `0x1C` | uint128_t 数组 | 16 | 无符号 128 位整数数组 |
+| `0x1D` | int256_t 数组 | 32 | 有符号 256 位整数数组 |
+| `0x1E` | uint256_t 数组 | 32 | 无符号 256 位整数数组 |
+| `0x1F` | 文本数组 | 1 | 文本字节数组,上位机按 UTF-8、GBK、ASCII 顺序自动解析 |
+
+enum 与结构体入口:
+
+| TYPE | 名称 | VALUE 格式 | 小程序处理 |
+|---:|---|---|---|
+| `0x20` | enum 变量 | `ENTRY_PREFIX + def_name_len8 + def_name + var_name_len8 + var_name` | 创建 enum 变量组,按 `def_name` 匹配 `typedef enum` |
+| `0x21` | 结构体实例 | `ENTRY_PREFIX + def_name_len8 + def_name + var_name_len8 + var_name` | 创建结构体组,按 `def_name` 匹配 `typedef struct` |
+| `0x22` | enum 数组 | `ENTRY_PREFIX + elem_byte_len16 + elem_count16 + dim_count8 + dim_len16[dim_count] + def_name_len8 + def_name + var_name_len8 + var_name` | 按元素展开,并按 `def_name` 匹配 `typedef enum` |
+| `0x23` | 结构体数组 | `ENTRY_PREFIX + elem_byte_len16 + elem_count16 + dim_count8 + dim_len16[dim_count] + def_name_len8 + def_name + var_name_len8 + var_name` | 导入结构体定义后按 `var[i].field` 或 `var[i][j].field` 展开 |
+
+基础变量 TLV 的 `LEN`:
+
+```text
+ADDR_WIDTH=16: LEN = 0x06 + var_name_len
+ADDR_WIDTH=32: LEN = 0x07 + var_name_len
+```
+
+enum 变量 TLV 的 `LEN`:
+
+```text
+ADDR_WIDTH=16: LEN = 0x07 + def_name_len + var_name_len
+ADDR_WIDTH=32: LEN = 0x08 + def_name_len + var_name_len
+```
+
+结构体实例 TLV 的 `LEN`:
+
+```text
+ADDR_WIDTH=16: LEN = 0x07 + def_name_len + var_name_len
+ADDR_WIDTH=32: LEN = 0x08 + def_name_len + var_name_len
+```
+
+数组变量 TLV 的 `LEN`:
+
+```text
+基础数组:
+ADDR_WIDTH=16: LEN = 0x0B + 2 * dim_count + var_name_len
+ADDR_WIDTH=32: LEN = 0x0C + 2 * dim_count + var_name_len
 
-因此 32 位地址固定内存入口 TLV 的 `LEN = 0x07 + name_len`,受 TLV 单项长度限制,`name_len` 最大为 248。
+enum/结构体数组:
+ADDR_WIDTH=16: LEN = 0x0C + 2 * dim_count + def_name_len + var_name_len
+ADDR_WIDTH=32: LEN = 0x0D + 2 * dim_count + def_name_len + var_name_len
+```
 
-### 9.2 自定义 TLV
+数组入口约束:
 
-`0x20~0x3F` 为自定义 TLV 区域。上位机保留原始 TLV 项展示,当前不对自定义项做业务解析。
+- `byte_len = elem_byte_len * elem_count`。
+- `dim_len[0] * dim_len[1] * ... * dim_len[dim_count - 1] = elem_count`。
+- 基础数组和文本数组不携带 `def_name`。
+- enum 数组必须携带 enum typedef 定义名。
+- 结构体数组必须携带 struct typedef 定义名。
+- 多维数组按 C 语言连续存储顺序展开,即最后一维变化最快。
+- 文本变量除 ASCII 单字符外都应使用文本数组;文本数组的 `elem_byte_len` 固定为 1,`byte_len` 为整个文本缓冲区字节数。
 
-### 9.3 板卡信息 TLV
+多维数组寻址规则:
+
+```text
+2D: linear_index = i * dim_len[1] + j
+3D: linear_index = i * dim_len[1] * dim_len[2] + j * dim_len[2] + k
+通用: linear_index = Σ index[n] * Π dim_len[n+1..dim_count-1]
+element_addr = byte_addr + linear_index * elem_byte_len
+struct_field_addr = byte_addr + linear_index * elem_byte_len + field_byte_offset
+```
+
+### 9.2 板卡信息 TLV
 
 板卡信息从 `0x40` 开始递增,全部为可选项。某块板不存在对应硬件信息时,不需要放该 TLV;小程序卡片和转换公式只使用实际同步到的字段。
 
@@ -294,8 +389,8 @@ TYPE LEN VALUE...
 | `0x43` | `rs_shunt` | 2 | `uint16_t` | 采样电阻,单位 mΩ,按 `ENDIAN_MARK` 解析 |
 | `0x44` | `bus_div` | 4 | `float32` | 母线电压分压比,按 `ENDIAN_MARK` 解析 |
 | `0x45` | `along_div` | 4 | `float32` | 模拟输入电压分压比,按 `ENDIAN_MARK` 解析 |
-| `0x46` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号,建议 0 结尾或 0 填充 |
-| `0x47` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号,建议最多 30 字节,可容纳至少 7 个常见 UTF-8 汉字 |
+| `0x46` | `chip_model` | 可变 | 文本字节串 | 芯片型号,建议 0 结尾或 0 填充;上位机按 UTF-8、GBK、ASCII 顺序自动解析 |
+| `0x47` | `model` | 可变 | 文本字节串 | 电机型号,建议最多 30 字节;上位机按 UTF-8、GBK、ASCII 顺序自动解析 |
 
 ## 10. 小程序同步后的处理
 
@@ -303,26 +398,30 @@ TYPE LEN VALUE...
 
 1. 按 `TYPE LEN VALUE` 遍历 TLV 项,未知类型跳过。
 2. 解析 `0x40` 起的可选板卡信息,更新通讯页 CodeInfo 卡片;不存在的字段不显示、不进入转换公式上下文。
-3. 遇到固定结构体入口 `TYPE=0x01/0x03/0x05/0x07` 创建结构体组;结构体定义未导入时按字节占位。
-4. 遇到固定变量入口 `TYPE=0x02/0x04/0x06/0x08` 创建单独变量组,初始按 `byte_len` 显示原始字节并标记为未配置。
-5. 按固定 TLV `TYPE` 解析内存区域和地址宽度。
-6. 根据描述符 `ENDIAN_MARK` 自动确定目标内存多字节值字节序:`55 AA` 为大端,`AA 55` 为小端;单字节值不受影响。
-7. 根据 `byte_addr`、`byte_len`、`TYPE`、`name` 创建参数组。
-8. 如果导入了 C 结构体和 enum 定义,只有结构体入口且 `name` 与结构体定义名一致、长度一致时才补全结构体字段;结构体字段类型为 enum 时,读回值按枚举项显示。
-9. 单独变量入口需要在 UI 中选择与 `byte_len` 一致的有符号/无符号整数或 `float` 类型;如果 `name` 匹配 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `name`,读回值按枚举项显示。
-10. 如果当前已有同地址、同区域、同名称、同长度的结构体组,并且已经导入过结构体定义,则保留当前导入结构,只更新来源地址、区域、轮询开关等状态。
-11. 如果存在相同 `name` 但区域或 `byte_addr` 不同的入口,应视为不同参数组。
+3. 先根据 CODEINFO 描述符 `ADDR_WIDTH` 确定内存入口地址格式:16 位入口解析 `area8 + byte_addr16`,32 位入口解析 `byte_addr32` 且区域视为 `ADDR32`。
+4. 遇到 `0x01..0x0F` 基础变量入口创建单变量组,按 TYPE 确定显示和写入类型;raw 类型只显示原始字节。
+5. 遇到 `0x10..0x1F` 基础数组入口创建数组组;普通数值数组按元素展开,文本数组按整段文本字段显示。
+6. 遇到 `TYPE=0x20` 创建 enum 变量组,按 `def_name` 匹配 `typedef enum`,读回值按枚举项显示。
+7. 遇到 `TYPE=0x21` 创建结构体组;结构体定义未导入时按字节占位,导入后按 `def_name` 与结构体定义名匹配并补全字段。
+8. 遇到 `TYPE=0x22` 创建 enum 数组组,按元素展开并按 `def_name` 匹配 enum。
+9. 遇到 `TYPE=0x23` 创建结构体数组组,导入 struct 后展开为 `var[i].field`、`var[i][j].field` 等。
+10. 根据描述符 `ENDIAN_MARK` 自动确定普通帧 `ADDR/LEN`、CodeInfo 内存入口多字节字段以及目标内存多字节值字节序:`55 AA` 为大端,`AA 55` 为小端;单字节值不受影响,CRC 始终低字节在前。
+11. 数组中的多字节元素按元素逐个应用 `ENDIAN_MARK`,不对整段数组字节整体反转。
+12. 如果当前已有同地址、同区域、同名称、同长度的结构体组,并且已经导入过结构体定义,则保留当前导入结构,只更新来源地址、区域、轮询开关等状态。
+13. 如果存在相同变量名但区域或 `byte_addr` 不同的入口,应视为不同参数组。
 
 ## 11. 示例
 
 ### 11.1 读取 XDATA 中 64 字节结构体
 
+以下示例使用 `ENDIAN_MARK=55 AA` 的大端设备;小端设备的 `ADDR/LEN` 字节顺序相反,CRC 仍固定低字节在前。
+
 ```text
 AREA = 0x03 XDATA
 ADDR = 0x2000
 LEN = 0x0040
-读请求 = 03 00 20 40 00 CRC_L CRC_H
-正常响应 = 03 00 20 40 00 DATA(64B) CRC_L CRC_H
+读请求 = 03 20 00 00 40 CRC_L CRC_H
+正常响应 = 03 20 00 00 40 DATA(64B) CRC_L CRC_H
 ```
 
 ### 11.2 读取 CodeInfo
@@ -334,7 +433,7 @@ TLV_ADDR = 0x00123456
 TLV_LEN  = 0x0120
 ADDR_WIDTH = 32
 MAX_PACKET = 0x0040
-ENDIAN_MARK = AA 55
+ENDIAN_MARK = 55 AA
 ```
 
 先读取 `CODEINFO` 描述符:
@@ -346,29 +445,67 @@ ENDIAN_MARK = AA 55
 再按 `ADDR_WIDTH=32` 读取 TLV 信息块:
 
 ```text
-07 56 34 12 00 20 01 CRC_L CRC_H
+07 00 12 34 56 01 20 CRC_L CRC_H
 ```
 
 `0x07` 表示 32 位统一地址模式。
 
 ### 11.3 CodeInfo TLV 示例
 
-结构体实例:
+16 位 XDATA 结构体实例:
 
 ```text
-TYPE = 0x05
-LEN = 0x14
-VALUE = 00 20 40 00 0F 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
-含义 = XDATA 0x2000 / 64 bytes / Motor_Runtime_t
+TYPE = 0x21
+LEN = 0x23
+VALUE = 03 20 00 00 40 0F 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74 0D 6D 6F 74 6F 72 5F 72 75 6E 74 69 6D 65
+含义 = XDATA 0x2000 / 64 bytes / typedef Motor_Runtime_t / 变量 motor_runtime
 ```
 
-单独变量:
+16 位 XDATA enum 变量:
 
 ```text
-TYPE = 0x08
+TYPE = 0x20
+LEN = 0x18
+VALUE = 03 21 00 00 02 09 52 75 6E 4D 6F 64 65 5F 74 08 72 75 6E 5F 6D 6F 64 65
+含义 = XDATA 0x2100 / 2 bytes / typedef RunMode_t / 变量 run_mode
+```
+
+32 位 `uint16_t speed_ref` 变量:
+
+```text
+TYPE = 0x05
 LEN = 0x10
-VALUE = 00 21 00 00 02 00 09 73 70 65 65 64 5F 72 65 66
-含义 = ADDR32 0x00002100 / 2 bytes / speed_ref
+VALUE = 00 00 21 00 00 02 09 73 70 65 65 64 5F 72 65 66
+含义 = ADDR32 0x00002100 / 2 bytes / uint16_t speed_ref
+```
+
+16 位 XDATA `uint16_t adc_table[2][3]`:
+
+```text
+TYPE = 0x14
+LEN = 0x18
+VALUE = 03 21 00 00 0C 00 02 00 06 02 00 02 00 03 09 61 64 63 5F 74 61 62 6C 65
+含义 = XDATA 0x2100 / 12 bytes / uint16_t adc_table[2][3]
+展开 = adc_table[0][0], adc_table[0][1], adc_table[0][2], adc_table[1][0], adc_table[1][1], adc_table[1][2]
+```
+
+16 位 XDATA `Motor_t motors[2][3]`:
+
+```text
+TYPE = 0x23
+LEN = 0x1D
+VALUE = 03 22 00 00 30 00 08 00 06 02 00 02 00 03 07 4D 6F 74 6F 72 5F 74 06 6D 6F 74 6F 72 73
+含义 = XDATA 0x2200 / 48 bytes / typedef Motor_t / 变量 motors[2][3] / sizeof(Motor_t)=8
+导入结构体后展开 = motors[0][0].field, motors[0][1].field, ... motors[1][2].field
+```
+
+16 位 XDATA 文本数组 `char model[7]`:
+
+```text
+TYPE = 0x1F
+LEN = 0x12
+VALUE = 03 23 00 00 07 00 01 00 07 01 00 07 05 6D 6F 64 65 6C
+含义 = XDATA 0x2300 / 7 bytes / 文本缓冲区 model;上位机按 UTF-8、GBK、ASCII 顺序解析读回字节
 ```
 
 ## 12. 实现约束
@@ -379,25 +516,18 @@ VALUE = 00 21 00 00 02 00 09 73 70 65 65 64 5F 72 65 66
 4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC,并确认数据长度等于 `LEN`。
 5. `CODEINFO` 和 `CODE` 区只读,写入必须返回 `WRITE_PROTECT`。
 6. 特殊指令正常响应不携带 `STATUS`,执行失败必须返回异常帧。
-7. CodeInfo 同步必须先读 `area=0x00 CODEINFO` 描述符,再按描述符返回的 `TLV_ADDR/TLV_LEN/ADDR_WIDTH/MAX_PACKET` 读取信息块;`ADDR_WIDTH=16` 使用 `area=0x04 CODE`,`ADDR_WIDTH=32` 使用 `area=0x07 ADDR32`,内存入口地址宽度由 TLV `TYPE` 决定。
+7. CodeInfo 同步必须先读 `area=0x00 CODEINFO` 描述符,再按描述符返回的 `TLV_ADDR/TLV_LEN/ADDR_WIDTH/MAX_PACKET` 读取信息块;`ADDR_WIDTH=16` 使用 `area=0x04 CODE`,`ADDR_WIDTH=32` 使用 `area=0x07 ADDR32`,内存入口地址宽度同样由描述符 `ADDR_WIDTH` 决定。
 8. 固件更新 CodeInfo TLV 类型或内存入口格式时,必须同步更新本文档和小程序解析器。
 
 ## 13. 读写位宽说明
 
-普通内存读写协议不额外携带变量位宽字段,只传输“字节地址 + 字节长度 + 字节数据”。变量显示和编辑位宽由以下信息决定:
+普通内存读写协议仍只传输“字节地址 + 字节长度 + 字节数据”。变量显示、编辑位宽和数组展开由 CodeInfo TLV 元数据决定:
 
-- `TYPE=0x01/0x03/0x05/0x07` 的结构体实例由导入的 C 结构体定义决定字段类型。
-- `TYPE=0x02/0x04/0x06/0x08` 的单独变量由 `byte_len` 给出字节长度,UI 必须选择与该长度一致的显示/编辑类型;未配置前只显示原始字节,不允许写入。
-- `enum` 不改变普通内存读写帧,只是在导入定义后给同长度整数值增加枚举映射;显示格式为 `枚举项 (数值)`,写入时可输入枚举项名称或数字。
+- `TYPE=0x01..0x0F` 的基础变量由 TYPE 和 `byte_len` 决定显示/编辑类型。
+- `TYPE=0x10..0x1F` 的基础数组由 TYPE、`elem_byte_len`、`elem_count` 和 `dim_len[]` 决定元素展开方式;文本数组按整段文本缓冲区显示。
+- `TYPE=0x20` 的 enum 变量按 `def_name` 匹配导入的 C enum 定义,列表右侧显示十进制值,左侧显示原始十六进制和匹配到的定义/枚举项。
+- `TYPE=0x21` 的结构体实例由导入的 C 结构体定义决定字段类型;未导入前按字节占位。
+- `TYPE=0x22` 的 enum 数组按元素展开并匹配 enum 定义。
+- `TYPE=0x23` 的结构体数组导入 C 定义后按 `var[i].field`、`var[i][j].field` 等形式展开。
+- raw 类型只显示原始字节,写入前需要在 UI 中进一步配置解释类型。
 - 参数页可在寄存器/变量配置中进一步设置公式、单位、范围和显示类型。
-
-如果后续从机需要原子读写、硬件寄存器位宽或对齐语义,可以新增 `bit6=1` 的 typed 特殊指令,不改变普通内存访问帧。具体 `special_code` 和 DATA 格式需另行定义。
-
-建议字段:
-
-| 字段 | 建议值 | 说明 |
-|---|---|---|
-| `ADDR` | 4 字节 | typed 扩展建议使用 32 位字节地址 |
-| `VALUE_WIDTH` | `0x01`、`0x02`、`0x04` | 单个值 8/16/32 位 |
-| `COUNT` | 16 位 | 值数量,不是字节数 |
-| `DATA` | `VALUE_WIDTH * COUNT` 字节 | 按协议小端序 |

+ 24 - 19
完整协议说明.md

@@ -87,7 +87,7 @@ SLAVE (FUNC | 0x80) EXCEPTION_CODE CRC_L CRC_H
 CMD ADDR LEN DATA... CRC_L CRC_H
 ```
 
-CRC 使用 `CRC16-CCITT-FALSE`,低字节在前。所有多字节协议控制字段均为小端序
+CRC 使用 `CRC16-CCITT-FALSE`,低字节在前。除 CRC 外,地址、长度和多字节数据字段都按 CodeInfo 描述符 `ENDIAN_MARK` 自动选择大端或小端;未同步时按设置页“默认大端模式”开关处理
 
 `CMD` 位定义:
 
@@ -119,6 +119,8 @@ CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
 CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
 ```
 
+上方普通帧示例按 `ENDIAN_MARK=AA 55` 的小端设备展示;`ENDIAN_MARK=55 AA` 时 `ADDR/LEN` 字节顺序反转,CRC 仍固定低字节在前。
+
 普通读响应回显 `CMD`、`ADDR`、`LEN`,然后携带 `LEN` 字节数据。
 
 普通写请求:
@@ -154,7 +156,7 @@ CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 1. 发送 `00 CRC_L CRC_H` 读取 `area=0x00 CODEINFO` 描述符,数据区为 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16 + ENDIAN_MARK16`。
 2. 根据描述符返回的地址、长度、地址位宽和最大包长读取完整 CodeInfo TLV 信息块;`ADDR_WIDTH=16` 使用 `CODE` 区 16 位地址,`ADDR_WIDTH=32` 使用 `ADDR32`。
 
-多字节协议控制字段始终固定小端,包括 `ADDR`、普通读写 `LEN`、描述符 `TLV_ADDR/TLV_LEN/MAX_PACKET` 和固定内存入口 `byte_addr/byte_len`;TLV `LEN` 与 `name_len` 为单字节字段,不涉及大小端。描述符末尾 `ENDIAN_MARK` 按原始两字节识别目标内存值字节序:`55 AA` 为大端,`AA 55` 为小端;大端时目标内存多字节值按大端解析,小端时参数组多字节值自动按小端编解码
+除 CRC 固定低字节在前外,其余地址、长度和多字节数据字段都跟随描述符末尾的原始两字节 `ENDIAN_MARK`:`55 AA` 为大端,`AA 55` 为小端。上位机先识别该标记,再按标记解析描述符 `TLV_ADDR/TLV_LEN/MAX_PACKET`、普通帧 `ADDR/LEN`、CodeInfo 内存入口的 `byte_addr/byte_len/elem_byte_len/elem_count/dim_len` 和参数组多字节值;TLV `TYPE/LEN`、`area`、`name_len`、`def_name_len`、`var_name_len` 和 `dim_count` 为单字节字段,不涉及大小端
 
 CodeInfo 信息块使用纯 TLV 格式,不再包含固定头、`format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`:
 
@@ -166,39 +168,42 @@ TYPE LEN VALUE...
 
 | TYPE | 名称 | VALUE |
 |---:|---|---|
-| `0x01` | DATA 结构体实例 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x02` | DATA 单独变量 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x03` | IDATA 结构体实例 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x04` | IDATA 单独变量 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x05` | XDATA 结构体实例 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x06` | XDATA 单独变量 | `byte_addr16 + byte_len16 + name_len8 + name`,`LEN=0x05+name_len` |
-| `0x07` | ADDR32 结构体实例 | `byte_addr32 + byte_len16 + name_len8 + name`,`LEN=0x07+name_len` |
-| `0x08` | ADDR32 单独变量 | `byte_addr32 + byte_len16 + name_len8 + name`,`LEN=0x07+name_len` |
-| `0x20..0x3F` | 自定义 TLV | 上位机保留原始项,当前跳过业务解析 |
+| `0x01..0x0F` | 基础变量 | `ENTRY_PREFIX + var_name_len8 + var_name` |
+| `0x10..0x1F` | 基础数组 | `ENTRY_PREFIX + elem_byte_len16 + elem_count16 + dim_count8 + dim_len16[] + var_name_len8 + var_name` |
+| `0x20` | enum 变量 | `ENTRY_PREFIX + def_name_len8 + def_name + var_name_len8 + var_name` |
+| `0x21` | 结构体实例 | `ENTRY_PREFIX + def_name_len8 + def_name + var_name_len8 + var_name` |
+| `0x22` | enum 数组 | `ENTRY_PREFIX + elem_byte_len16 + elem_count16 + dim_count8 + dim_len16[] + def_name_len8 + def_name + var_name_len8 + var_name` |
+| `0x23` | 结构体数组 | `ENTRY_PREFIX + elem_byte_len16 + elem_count16 + dim_count8 + dim_len16[] + def_name_len8 + def_name + var_name_len8 + var_name` |
 | `0x40..` | 板卡参数 | `0x40` 起按 `cave_freq/ref_volt/...` 递增 |
 
-TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 CODEINFO 描述符 `TLV_LEN16` 决定。内存入口 TLV 的地址宽度和区域由 `TYPE` 决定,`VALUE` 中的 `byte_addr/byte_len` 按小端序解析,名称字段由 `name_len` 声明,名称本身为 UTF-8 或 ASCII 字节,不再固定 32 字节。
+`ENTRY_PREFIX` 由 CODEINFO 描述符 `ADDR_WIDTH` 决定:`ADDR_WIDTH=16` 时为 `area8 + byte_addr16 + byte_len16`,其中 `area` 为 `DATA/IDATA/XDATA/CODE`;`ADDR_WIDTH=32` 时为 `byte_addr32 + byte_len16`,区域固定为 `ADDR32`。TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 CODEINFO 描述符 `TLV_LEN16` 决定。`byte_addr/byte_len/elem_byte_len/elem_count/dim_len` 按 `ENDIAN_MARK` 解析。
+
+基础变量和基础数组的类型由 TYPE 直接决定,覆盖 raw、8/16/32/64/128/256 位有符号和无符号整数、float32、double,以及文本数组。文本数组按 UTF-8、GBK、ASCII 顺序自动解析;除 ASCII 单字符外,文本都按数组形式描述。未定义 TYPE 可由项目自定义,通用解析器只保留原始项并跳过业务解析。
 
 同步到参数页后的规则:
 
 | 条件 | 处理 |
 |---|---|
-| `TYPE=0x01/0x03/0x05/0x07` 且未导入结构体定义 | 创建结构体组,每个字节按 `00`、`01`、`02` 命名 |
-| `TYPE=0x01/0x03/0x05/0x07` 且导入定义名、长度匹配 | 按 C 结构体字段展示,保留多字节字段整体值;字段类型为 enum 时按枚举项显示 |
-| `TYPE=0x02/0x04/0x06/0x08` | 创建单变量组,`byte_len` 只定义长度;未配置前显示原始字节,UI 需选择同长度的有符号/无符号整数或 `float`,匹配 enum 后按枚举项显示 |
+| `TYPE=0x01..0x0F` | 创建基础单变量组,按 TYPE 解码;raw 类型只显示原始字节 |
+| `TYPE=0x10..0x1F` | 基础数组按 `dim_len[]` 展开为 `var[i]` 或 `var[i][j]`;文本数组显示为整段文本字段 |
+| `TYPE=0x20` 且 `def_name` 匹配 enum 定义 | 创建 enum 变量组,列表左侧显示存放位置、起始地址、长度、原始十六进制和枚举定义/枚举项,右侧显示十进制值 |
+| `TYPE=0x21` 且未导入结构体定义 | 创建结构体组,每个字节按 `00`、`01`、`02` 命名 |
+| `TYPE=0x21` 且 `def_name` 与导入定义名、长度匹配 | 按 C 结构体字段展示,保留多字节字段整体值;字段类型为 enum 时按枚举项显示 |
+| `TYPE=0x22` enum 数组 | 按元素展开并按 `def_name` 匹配 enum 定义 |
+| `TYPE=0x23` 结构体数组 | 导入 C struct 后展开为 `var[i].field`、`var[i][j].field` 等,未导入前按字节占位 |
 | 相同名称但地址或区域不同 | 视为不同参数组 |
 | 当前已有同区域、同地址、同名称、同长度且已导入定义 | 保留当前导入结构,只更新同步来源信息 |
 | 未知 TLV `TYPE` | 跳过 |
 
-结构体和 enum 可在同一个 `.h/.c/.txt` 定义文件中导入。单独变量的 enum 匹配支持两种方式:入口 `name` 直接等于 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名等于入口 `name`。enum 只影响显示与输入映射,不改变底层字节读写协议。
+结构体和 enum 可在同一个 `.h/.c/.txt` 定义文件中导入。结构体入口按 `def_name` 匹配 `typedef struct` 名称,enum 入口按 `def_name` 匹配 `typedef enum` 名称;`var_name` 只作为参数页显示名和去重信息。enum 只影响显示与输入映射,不改变底层字节读写协议。
 
-CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信息 TLV 不存在时对应字段不显示;电机型号优先按 UTF-8 解析,也兼容纯 ASCII
+CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信息 TLV 不存在时对应字段不显示;电机型号和芯片型号按 UTF-8、GBK、ASCII 顺序自动解析
 
 ## 7. 参数值与转换公式
 
 参数页读回的数据先按字段类型解码为原始值,再根据可选转换公式显示实际值。公式由参数配置自定义,适合标幺值转实际值、比例缩放和偏置换算。
 
-存储访问参数组的多字节变量值按 CodeInfo 描述符 `ENDIAN_MARK` 自动选择大端或小端解码和写入;未同步 CodeInfo 时默认小端。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
+存储访问参数组的多字节变量值按 CodeInfo 描述符 `ENDIAN_MARK` 自动选择大端或小端解码和写入;未同步 CodeInfo 时按设置页“默认大端模式”开关处理。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
 
 公式支持变量:
 
@@ -213,7 +218,7 @@ CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信
 | `alongDiv` | CodeInfo 模拟输入电压分压比 |
 | `maxPacketLength` | 当前同步上下文的最大完整协议帧长度,未声明时为 0 |
 | `addressWidth` | CodeInfo 本体读取地址长度,来自描述符 `ADDR_WIDTH` |
-| `memoryEndian` | 当前目标内存变量字节序,来自描述符 `ENDIAN_MARK`;未同步时默认 `little` |
+| `memoryEndian` | 当前目标内存变量字节序,来自描述符 `ENDIAN_MARK`;未同步时来自设置页默认字节序 |
 
 公式只支持数字、括号和 `+ - * /`,不执行任意脚本。
 

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác