avery 5 päivää sitten
vanhempi
commit
ca3771c878

+ 7 - 0
domain/parameter-groups/constants.js

@@ -103,6 +103,13 @@ const DATA_TYPE_OPTIONS = [
     label: 'HEX',
     kind: 'hex',
     wordCount: 1
+  },
+  {
+    byteLength: 1,
+    key: 'raw',
+    label: '未配置',
+    kind: 'raw',
+    wordCount: 1
   }
 ]
 

+ 5 - 0
domain/parameter-groups/model.js

@@ -238,9 +238,14 @@ function createRegisterSourceMetaText(register) {
   const bitText = isBitFieldRegister(register)
     ? `bit${normalizeBitOffset(register.bitOffset)}:${normalizeBitWidth(register.bitWidth)}`
     : ''
+  const sourceByteLength = Number(register.sourceByteLength)
+  const sourceByteLengthText = Number.isFinite(sourceByteLength) && sourceByteLength > 0
+    ? `${Math.floor(sourceByteLength)}B`
+    : ''
   const parts = [
     register.sourceMemoryArea,
     register.sourceAddressText,
+    sourceByteLengthText,
     bitText,
     register.sourceSymbolType && register.sourceSymbolType !== '---' ? register.sourceSymbolType : ''
   ].filter(Boolean)

+ 12 - 1
domain/parameter-groups/value-codec.js

@@ -22,6 +22,7 @@ const {
   isByteRegister,
   isHexRegister,
   isNumericRegister,
+  isRawRegister,
   isTextRegister,
   normalizeBitOffset,
   normalizeBitWidth,
@@ -228,6 +229,7 @@ function encodeRegisterBytes(register) {
 
     return paddedBytes.slice(0, byteLength)
   }
+  if (isRawRegister(dataType)) return null
 
   let numberValue = parseEnumValueText(register, valueText)
   if (numberValue === null) numberValue = parseNumberText(valueText, dataType)
@@ -275,6 +277,9 @@ function decodeRegisterValue(register, words) {
   if (isTextRegister(dataType)) {
     return decodeTextBytes(bytes.slice(0, getEncodeByteLimit(register)), dataType)
   }
+  if (isRawRegister(dataType)) {
+    return bytes.slice(0, byteLength)
+  }
   if (dataType === 'float') {
     return bytesToFloatValue(bytes, memoryEndian)
   }
@@ -305,10 +310,12 @@ function decodeRegisterValue(register, words) {
 function formatRegisterValue(register, rawValue) {
   if (rawValue === null || rawValue === undefined) return '--'
 
+  const dataType = getDataType(register.dataType).key
+  if (isRawRegister(dataType)) return formatRawByteText(Array.isArray(rawValue) ? rawValue : [])
+
   const enumValue = formatEnumValue(register, rawValue)
   if (enumValue) return enumValue
 
-  const dataType = getDataType(register.dataType).key
   if (isTextRegister(dataType)) return normalizeTextValue(rawValue)
   if (dataType === 'hex') return formatHexValue(rawValue)
   if (dataType === 'float') return formatFloatValue(rawValue)
@@ -345,6 +352,9 @@ function validateRegisterValue(register, value) {
   }
 
   const dataType = getDataType(register.dataType).key
+  if (isRawRegister(dataType)) {
+    throw new Error(`${register.name || '寄存器'} 需要先配置数据类型`)
+  }
   if (isTextRegister(dataType)) {
     encodeTextBytes(valueText, dataType, getEncodeByteLimit(register))
     return true
@@ -388,6 +398,7 @@ module.exports = {
   isByteRegister,
   isHexRegister,
   isNumericRegister,
+  isRawRegister,
   isTextRegister,
   normalizeBitOffset,
   normalizeBitWidth,

+ 25 - 0
domain/parameter-groups/value-types.js

@@ -33,6 +33,24 @@ function getRegisterTextByteLength(register = {}) {
   return normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
 }
 
+function getRegisterRawByteLength(register = {}) {
+  const candidates = [
+    register.sourceByteLength,
+    register.rawByteLength,
+    register.textByteLength,
+    register.byteLength
+  ]
+
+  for (const value of candidates) {
+    const numberValue = Number(value)
+    if (Number.isFinite(numberValue) && numberValue > 0) {
+      return Math.max(1, Math.floor(numberValue))
+    }
+  }
+
+  return 1
+}
+
 function isStructLayout(layout) {
   return layout === GROUP_LAYOUT_STRUCT
 }
@@ -70,6 +88,7 @@ function getRegisterByteLength(dataType, register = {}) {
   if (isBitFieldRegister(register)) return getBitFieldByteLength(register)
 
   const type = getDataType(dataType)
+  if (type.kind === 'raw') return getRegisterRawByteLength(register)
   if (type.kind === 'text') {
     const byteLength = getRegisterTextByteLength(register)
 
@@ -114,6 +133,10 @@ function isHexRegister(dataType) {
   return getDataType(dataType).key === 'hex'
 }
 
+function isRawRegister(dataType) {
+  return getDataType(dataType).kind === 'raw'
+}
+
 function isNumericRegister(dataType) {
   return getDataType(dataType).kind === 'number'
 }
@@ -138,6 +161,7 @@ module.exports = {
   getDataTypeIndex,
   getEncodeByteLimit,
   getRegisterByteLength,
+  getRegisterRawByteLength,
   getRegisterTextByteLength,
   getRegisterValueTypeLabel,
   getRegisterWordCount,
@@ -147,6 +171,7 @@ module.exports = {
   isByteRegister,
   isHexRegister,
   isNumericRegister,
+  isRawRegister,
   isTextRegister,
   normalizeBitOffset,
   normalizeBitWidth,

+ 89 - 54
domain/storage-access/code-info-parser.js

@@ -5,21 +5,34 @@ const {
 } = require('../../utils/binary-utils.js')
 
 const CODE_INFO_TLV_HEADER_BYTE_LENGTH = 2
+const CODE_INFO_ENTRY_NAME_BYTE_LENGTH = 32
 const CODE_INFO_TLV = {
-  CAVE_FREQ: 0x01,
-  REF_VOLT: 0x02,
-  AMP_GAIN: 0x03,
-  RS_SHUNT: 0x04,
-  BUS_DIV: 0x05,
-  ALONG_DIV: 0x06,
-  CHIP_MODEL: 0x07,
-  MODEL: 0x08,
-  STRUCT_ENTRY16: 0x20,
-  VARIABLE_ENTRY16: 0x21,
-  STRUCT_ENTRY32: 0x28,
-  VARIABLE_ENTRY32: 0x29
+  DATA_STRUCT: 0x01,
+  DATA_VARIABLE: 0x02,
+  IDATA_STRUCT: 0x03,
+  IDATA_VARIABLE: 0x04,
+  XDATA_STRUCT: 0x05,
+  XDATA_VARIABLE: 0x06,
+  ADDR32_STRUCT: 0x07,
+  ADDR32_VARIABLE: 0x08,
+  CAVE_FREQ: 0x40,
+  REF_VOLT: 0x41,
+  AMP_GAIN: 0x42,
+  RS_SHUNT: 0x43,
+  BUS_DIV: 0x44,
+  ALONG_DIV: 0x45,
+  CHIP_MODEL: 0x46,
+  MODEL: 0x47
 }
 const CODE_INFO_TLV_NAMES = {
+  [CODE_INFO_TLV.DATA_STRUCT]: 'data_struct',
+  [CODE_INFO_TLV.DATA_VARIABLE]: 'data_var',
+  [CODE_INFO_TLV.IDATA_STRUCT]: 'idata_struct',
+  [CODE_INFO_TLV.IDATA_VARIABLE]: 'idata_var',
+  [CODE_INFO_TLV.XDATA_STRUCT]: 'xdata_struct',
+  [CODE_INFO_TLV.XDATA_VARIABLE]: 'xdata_var',
+  [CODE_INFO_TLV.ADDR32_STRUCT]: 'addr32_struct',
+  [CODE_INFO_TLV.ADDR32_VARIABLE]: 'addr32_var',
   [CODE_INFO_TLV.CAVE_FREQ]: 'cave_freq',
   [CODE_INFO_TLV.REF_VOLT]: 'ref_volt',
   [CODE_INFO_TLV.AMP_GAIN]: 'amp_gain',
@@ -27,11 +40,7 @@ const CODE_INFO_TLV_NAMES = {
   [CODE_INFO_TLV.BUS_DIV]: 'bus_div',
   [CODE_INFO_TLV.ALONG_DIV]: 'along_div',
   [CODE_INFO_TLV.CHIP_MODEL]: 'chip_model',
-  [CODE_INFO_TLV.MODEL]: 'model',
-  [CODE_INFO_TLV.STRUCT_ENTRY16]: 'struct_entry16',
-  [CODE_INFO_TLV.VARIABLE_ENTRY16]: 'variable_entry16',
-  [CODE_INFO_TLV.STRUCT_ENTRY32]: 'struct_entry32',
-  [CODE_INFO_TLV.VARIABLE_ENTRY32]: 'variable_entry32'
+  [CODE_INFO_TLV.MODEL]: 'model'
 }
 const CODE_INFO_ENTRY_KIND = {
   STRUCT: 0x00,
@@ -41,11 +50,47 @@ const CODE_INFO_ENTRY_KIND_TEXT = {
   [CODE_INFO_ENTRY_KIND.STRUCT]: 'struct',
   [CODE_INFO_ENTRY_KIND.VARIABLE]: 'variable'
 }
-const MEMORY_TYPE_AREAS = {
-  0x01: 'DATA',
-  0x02: 'IDATA',
-  0x03: 'XDATA',
-  0x04: 'CODE'
+const CODE_INFO_ENTRY_LAYOUTS = {
+  [CODE_INFO_TLV.DATA_STRUCT]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.STRUCT,
+    memoryArea: 'DATA'
+  },
+  [CODE_INFO_TLV.DATA_VARIABLE]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.VARIABLE,
+    memoryArea: 'DATA'
+  },
+  [CODE_INFO_TLV.IDATA_STRUCT]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.STRUCT,
+    memoryArea: 'IDATA'
+  },
+  [CODE_INFO_TLV.IDATA_VARIABLE]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.VARIABLE,
+    memoryArea: 'IDATA'
+  },
+  [CODE_INFO_TLV.XDATA_STRUCT]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.STRUCT,
+    memoryArea: 'XDATA'
+  },
+  [CODE_INFO_TLV.XDATA_VARIABLE]: {
+    addressWidth: 16,
+    entryKind: CODE_INFO_ENTRY_KIND.VARIABLE,
+    memoryArea: 'XDATA'
+  },
+  [CODE_INFO_TLV.ADDR32_STRUCT]: {
+    addressWidth: 32,
+    entryKind: CODE_INFO_ENTRY_KIND.STRUCT,
+    memoryArea: 'ADDR32'
+  },
+  [CODE_INFO_TLV.ADDR32_VARIABLE]: {
+    addressWidth: 32,
+    entryKind: CODE_INFO_ENTRY_KIND.VARIABLE,
+    memoryArea: 'ADDR32'
+  }
 }
 const MEMORY_ENDIAN = {
   BIG: 'big',
@@ -99,21 +144,22 @@ function normalizeCodeInfoAddressWidth(value) {
 
 function getEntryLayout(type) {
   const entryType = Number(type) & 0xFF
-  const isStructEntry = entryType === CODE_INFO_TLV.STRUCT_ENTRY16 || entryType === CODE_INFO_TLV.STRUCT_ENTRY32
-  const isVariableEntry = entryType === CODE_INFO_TLV.VARIABLE_ENTRY16 || entryType === CODE_INFO_TLV.VARIABLE_ENTRY32
-  if (!isStructEntry && !isVariableEntry) return null
+  const layout = CODE_INFO_ENTRY_LAYOUTS[entryType]
+  if (!layout) return null
 
-  const addressWidth = entryType === CODE_INFO_TLV.STRUCT_ENTRY32 || entryType === CODE_INFO_TLV.VARIABLE_ENTRY32
-    ? 32
-    : 16
+  const addressWidth = layout.addressWidth
   const addressByteLength = addressWidth === 16 ? 2 : 4
+  const valueByteLength = addressByteLength + 2 + CODE_INFO_ENTRY_NAME_BYTE_LENGTH
 
   return {
     addressByteLength,
     addressWidth,
-    entryKind: isStructEntry ? CODE_INFO_ENTRY_KIND.STRUCT : CODE_INFO_ENTRY_KIND.VARIABLE,
-    hasMemoryType: addressWidth === 16,
-    minByteLength: addressWidth === 16 ? 5 : 6
+    entryKind: layout.entryKind,
+    hasMemoryType: false,
+    memoryArea: layout.memoryArea,
+    nameByteLength: CODE_INFO_ENTRY_NAME_BYTE_LENGTH,
+    nameOffset: addressByteLength + 2,
+    valueByteLength
   }
 }
 
@@ -157,27 +203,24 @@ function resolveCodeInfoByteLength(sourceLength, options = {}) {
 function parseMemoryEntry(valueBytes, index, type) {
   const layout = getEntryLayout(type)
   if (!layout) return null
-  if (valueBytes.length < layout.minByteLength) {
-    throw new Error('CodeInfo 内存入口 TLV 长度无效')
+  if (valueBytes.length !== layout.valueByteLength) {
+    throw new Error(`CodeInfo 内存入口 TLV 长度无效,期望 ${layout.valueByteLength} 字节`)
   }
 
-  const memType = layout.hasMemoryType ? (valueBytes[0] & 0xFF) : 0
-  const addressOffset = layout.hasMemoryType ? 1 : 0
+  const memType = 0
+  const addressOffset = 0
   const byteAddr = layout.addressByteLength === 2
     ? readUint16(valueBytes, addressOffset)
     : readUint32(valueBytes, addressOffset)
   const byteLength = readUint16(valueBytes, addressOffset + layout.addressByteLength)
-  const nameOffset = layout.minByteLength
+  const nameOffset = layout.nameOffset
   const entryKind = layout.entryKind
   const entryKindText = getEntryKindText(entryKind)
-  const nameByteLength = Math.max(0, valueBytes.length - layout.minByteLength)
   const typeName = normalizeTypeName(
-    readTlvText(valueBytes.slice(nameOffset, nameOffset + nameByteLength)),
+    readTlvText(valueBytes.slice(nameOffset, nameOffset + layout.nameByteLength)),
     `${entryKindText === 'variable' ? 'var' : 'struct'}_${index + 1}`
   )
-  const memoryArea = layout.addressWidth === 32
-    ? 'ADDR32'
-    : (MEMORY_TYPE_AREAS[memType] || 'UNKNOWN')
+  const memoryArea = layout.memoryArea || 'UNKNOWN'
 
   return {
     addressByteLength: layout.addressByteLength,
@@ -232,6 +275,9 @@ function applyCodeInfoTlvValue(target, type, valueBytes) {
       target.model = readTlvText(valueBytes)
       break
     default:
+      if (type >= 0x20 && type < 0x40) {
+        target.customTlvCount = (Number(target.customTlvCount) || 0) + 1
+      }
       break
   }
 }
@@ -337,22 +383,10 @@ function createRegistersForByteSpan(entry) {
   return registers
 }
 
-function inferVariableDataType(byteLength) {
-  const length = Number(byteLength)
-  if (length === 1) return 'uint8_t'
-  if (length === 2) return 'uint16_t'
-  if (length === 4) return 'uint32_t'
-
-  return ''
-}
-
 function createVariableRegisters(entry) {
-  const dataType = inferVariableDataType(entry.byteLength)
-  if (!dataType) return createRegistersForByteSpan(entry)
-
   return [{
     byteStart: 0,
-    dataType,
+    dataType: 'raw',
     name: entry.typeName,
     sourceAddress: entry.byteAddr,
     sourceAddressByteLength: entry.addressByteLength,
@@ -440,6 +474,7 @@ function createGroupsFromCodeInfo(codeInfo, options = {}) {
 
 module.exports = {
   CODE_INFO_ENTRY_KIND,
+  CODE_INFO_ENTRY_NAME_BYTE_LENGTH,
   CODE_INFO_TLV_HEADER_BYTE_LENGTH,
   CODE_INFO_TLV,
   MEMORY_ENDIAN,

+ 5 - 3
features/communication/manual-rtu.js

@@ -19,6 +19,8 @@ const {
   validateRegisterValue
 } = require('../../domain/parameter-groups/model.js')
 
+const MANUAL_DATA_TYPE_OPTIONS = DATA_TYPE_OPTIONS.filter((item) => item.key !== 'raw')
+
 const MODBUS_COMMANDS = [
   { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' },
   { key: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, inputMode: 'quantity' },
@@ -37,7 +39,7 @@ const state = {
   coilEnabled: true,
   generatedHex: '',
   protocolCommands: MODBUS_COMMANDS,
-  protocolDataTypeOptions: DATA_TYPE_OPTIONS,
+  protocolDataTypeOptions: MANUAL_DATA_TYPE_OPTIONS,
   protocolErrorText: '',
   protocolMultipleDialog: {
     visible: false
@@ -202,7 +204,7 @@ function createManualMultipleRegister(index, value = {}) {
 
   return {
     ...register,
-    dataTypeIndex: DATA_TYPE_OPTIONS.findIndex((item) => item.key === register.dataType),
+    dataTypeIndex: Math.max(0, MANUAL_DATA_TYPE_OPTIONS.findIndex((item) => item.key === register.dataType)),
     inputValue: value.inputValue === undefined ? '' : value.inputValue
   }
 }
@@ -267,7 +269,7 @@ function getManualMultipleValueText(values = []) {
 }
 
 function getManualMultipleDataType(dataTypeIndex) {
-  return DATA_TYPE_OPTIONS[Number(dataTypeIndex)] || DATA_TYPE_OPTIONS[0]
+  return MANUAL_DATA_TYPE_OPTIONS[Number(dataTypeIndex)] || MANUAL_DATA_TYPE_OPTIONS[0]
 }
 
 function updateManualMultipleValue(values = [], index, value) {

+ 0 - 7
features/communication/service.js

@@ -106,13 +106,6 @@ async function executeStorageAccessSpecialCommand(command = {}, data = {}) {
     }
   }
 
-  if (command.key === 'controlRef' && data.storageAccessControlRefErrorText) {
-    return {
-      errorText: data.storageAccessControlRefErrorText,
-      ok: false
-    }
-  }
-
   return storageAccessService.executeControlCommand(command.key, data, {
     maxPacketLength: data.parameterMaxPacketLength,
     showModal: true

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

@@ -10,6 +10,9 @@ const {
 
 function getRegisterByteLengthFromConfig(register) {
   const dataType = getDataType(register.dataType).key
+  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 === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
   if (dataType === 'int8_t' || dataType === 'uint8_t') return 1
@@ -151,13 +154,13 @@ function findEnumCompletion(group, catalog = {}) {
   )) || null
 }
 
-function getIntegerDataTypeForByteLength(byteLength, fallback = 'uint16_t') {
+function getIntegerDataTypeForByteLength(byteLength) {
   const length = Number(byteLength)
   if (length === 1) return 'uint8_t'
   if (length === 2) return 'uint16_t'
   if (length === 4) return 'uint32_t'
 
-  return fallback
+  return ''
 }
 
 function cloneEnumOptions(enumInfo) {
@@ -174,16 +177,23 @@ function completeEnumVariableGroup(group, enumInfo) {
   const enumOptions = cloneEnumOptions(enumInfo)
   if (!enumOptions.length) return group
 
-  const registers = (Array.isArray(group.registers) ? group.registers : []).map((register) => ({
-    ...register,
-    dataType: getIntegerDataTypeForByteLength(
-      register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength,
-      register.dataType || enumInfo.dataType
-    ),
-    enumName: enumInfo.name,
-    enumOptions,
-    sourceSymbolType: enumInfo.name || register.sourceSymbolType
-  }))
+  const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
+  const registers = sourceRegisters.map((register) => {
+    const dataType = getIntegerDataTypeForByteLength(
+      register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength
+    )
+
+    return dataType
+      ? {
+        ...register,
+        dataType,
+        enumName: enumInfo.name,
+        enumOptions,
+        sourceSymbolType: enumInfo.name || register.sourceSymbolType
+      }
+      : register
+  })
+  if (!registers.some((register, index) => register !== sourceRegisters[index])) return group
 
   return normalizeGroup({
     ...group,

+ 10 - 6
features/parameter-groups/io.js

@@ -30,7 +30,7 @@ const {
   splitWordSpans
 } = require('../../domain/parameter-groups/model.js')
 
-const STORAGE_ACCESS_CODE_AREA = storageAccessService.AREA.CODE
+const STORAGE_ACCESS_READ_ONLY_AREAS = [storageAccessService.AREA.CODEINFO, storageAccessService.AREA.CODE]
 const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
 const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF
 
@@ -41,7 +41,11 @@ function getMemoryType(group = {}) {
 
   const area = storageAccessService.AREA[memoryArea]
 
-  return area || null
+  return area === undefined ? null : area
+}
+
+function isReadOnlyMemoryType(memoryType) {
+  return STORAGE_ACCESS_READ_ONLY_AREAS.indexOf(memoryType) >= 0
 }
 
 function isMemoryGroup(group = {}) {
@@ -432,8 +436,8 @@ async function writeMemoryRegister(group, register, options = {}) {
     transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
     return null
   }
-  if (memoryType === STORAGE_ACCESS_CODE_AREA) {
-    transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
+  if (isReadOnlyMemoryType(memoryType)) {
+    transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`)
     return null
   }
 
@@ -539,8 +543,8 @@ async function writeMemoryGroup(group, options = {}) {
     transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
     return null
   }
-  if (memoryType === STORAGE_ACCESS_CODE_AREA) {
-    transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
+  if (isReadOnlyMemoryType(memoryType)) {
+    transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`)
     return null
   }
 

+ 31 - 1
features/parameter-groups/service.js

@@ -21,6 +21,7 @@ const {
   MAX_STORAGE_ADDRESS,
   REGISTER_TYPE_OPTIONS,
   cloneImportedGroup,
+  getDataType,
   isAddressRangeOverflow,
   normalizeGroup,
   normalizeGroupConfig,
@@ -65,6 +66,26 @@ function getAddressOverflowText(protocolMode = getActiveProtocolMode()) {
     : '地址范围超出 0xFFFF'
 }
 
+function isUnconfiguredRegister(register = {}) {
+  return getDataType(register.dataType).key === 'raw'
+}
+
+function getUnconfiguredRegisterName(register = {}, index = 0) {
+  return register.name || `变量 ${index + 1}`
+}
+
+function findUnconfiguredRegister(group = {}) {
+  const registers = Array.isArray(group.registers) ? group.registers : []
+  const index = registers.findIndex(isUnconfiguredRegister)
+
+  return index >= 0
+    ? {
+      index,
+      register: registers[index]
+    }
+    : null
+}
+
 function initParameterGroups() {
   settingsService.init()
   init(getActiveProtocolMode())
@@ -335,7 +356,7 @@ function updateRegister(groupId, registerIndex, changedData) {
         currentIndex === registerIndex
           ? {
             ...register,
-            ...(shouldResetReadState ? { rawValue: null, rawWords: [] } : {}),
+            ...(shouldResetReadState ? { rawBytes: [], rawValue: null, rawWords: [] } : {}),
             ...changedData
           }
           : register
@@ -400,6 +421,10 @@ async function writeRegister(groupId, registerIndex) {
     transport.showCommandAlert('参数组写入', getAddressOverflowText(protocolMode))
     return false
   }
+  if (isUnconfiguredRegister(register)) {
+    transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(register, registerIndex)} 需要先配置数据类型`)
+    return false
+  }
 
   const writeResult = await parameterGroupIo.writeRegister(group, registerIndex, {
     useStorageAccess: isStorageAccessProtocolMode(protocolMode)
@@ -429,6 +454,11 @@ async function writeGroup(groupId, options = {}) {
     transport.showCommandAlert('参数组写入', getAddressOverflowText(protocolMode))
     return false
   }
+  const unconfigured = findUnconfiguredRegister(group)
+  if (unconfigured) {
+    transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(unconfigured.register, unconfigured.index)} 需要先配置数据类型`)
+    return false
+  }
 
   const writeResult = await parameterGroupIo.writeGroup(group, {
     ...options,

+ 35 - 0
features/parameter-groups/view-model.js

@@ -10,6 +10,36 @@ function getOption(options, index) {
   return options[Number(index)] || options[0] || {}
 }
 
+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
+
+  return Math.floor(byteLength)
+}
+
+function getDataTypeConfigByteLength(dataType = {}, dialog = {}) {
+  if (dataType.kind === 'raw') return getCodeInfoVariableByteLength(dialog)
+  if (dataType.kind === 'text') return Number(dialog.textByteLength) || Number(dataType.byteLength) || 0
+
+  return Number(dataType.byteLength) || Number(dataType.wordCount || 0) * 2
+}
+
+function validateCodeInfoVariableDataType(dialog = {}, dataType = {}) {
+  const sourceByteLength = getCodeInfoVariableByteLength(dialog)
+  if (!sourceByteLength || dataType.kind === 'raw') return
+
+  if (dataType.kind === 'text' || dataType.kind === 'hex') {
+    throw new Error('单独变量类型请选择有符号/无符号整数、float 或 enum 对应整数类型')
+  }
+
+  const dataTypeByteLength = getDataTypeConfigByteLength(dataType, dialog)
+  if (dataTypeByteLength !== sourceByteLength) {
+    throw new Error(`单独变量 TLV 长度为 ${sourceByteLength}B,不能选择 ${dataType.label || dataType.key || '该类型'}`)
+  }
+}
+
 function getPageState() {
   const settingsState = settingsService.getState()
   const transportState = transport.getState()
@@ -112,6 +142,8 @@ function createParameterDialogState(overrides = {}) {
     parsedStructRegisters: [],
     pollEnabled: true,
     showPollEnabled: false,
+    sourceByteLength: '',
+    sourceEntryKind: '',
     structDefinition: '',
     structParsedSummary: '',
     ...overrides
@@ -172,6 +204,8 @@ function createParameterRegisterDialogState(mode, group, register, registerIndex
     addressText: register.addressRangeText || register.addressText || '',
     displayValue: register.displayValue || '',
     rawValueText: register.rawValueText || '--',
+    sourceByteLength: register.sourceByteLength || '',
+    sourceEntryKind: register.sourceEntryKind || '',
     sourceMetaText: register.sourceMetaText || '',
     showDataType: !!register.showDataType,
     showRange: !!register.showRange,
@@ -233,6 +267,7 @@ function createParameterRegisterChangedData(dialog, dataTypeOptions) {
   const conversionFormula = String(dialog.conversionFormula || '').trim()
 
   validateValueFormula(conversionFormula)
+  validateCodeInfoVariableDataType(dialog, dataType)
 
   return {
     name: dialog.name,

+ 5 - 4
features/settings/protocol-implementation.js

@@ -11,10 +11,11 @@ const GUIDE = {
   title: '协议说明与实现规划',
   text: '存储访问协议普通读写使用 CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE 帧格式,不带从机地址。当前先固定上位机协议模型,从机源码参考暂不提供,后续再单独规划实现细节。',
   points: [
-    { id: 'frame', text: 'CMD bit7 为故障位,bit3 为读写位,bit4/bit5 暂时保留,bit0~bit2 表示地址模式或区域。' },
-    { id: 'addr', text: 'bit0~bit2 为 0x7 时下发 32 位地址;0x1~0x4 对应 DATA、IDATA、XDATA、CODE 的 16 位地址;0x0、0x5、0x6 保留。' },
-    { id: 'info', text: 'bit0、bit1、bit2、bit3、bit6 全为 1 时为特殊指令,即 CMD=0x4F;OP=0x05 返回 CodeInfo 地址、长度、CodeInfo 读取地址宽度、目标内存字节序与最大包长。' },
-    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;0x20/0x21 表示 16 位地址入口,0x28/0x29 表示 32 位地址入口。' },
+    { 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、地址位宽和最大包长。' },
+    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;0x01~0x08 为固定内存入口,VALUE 为 addr(2/4)+len16+name[32];单独变量由 TLV 给长度,UI 或 enum 导入配置解释类型。' },
     { id: 'source', text: '协议实现源码已暂时移除,设置页仅保留文件占位与说明,避免给出过期从机实现。' }
   ]
 }

+ 37 - 124
features/storage-access/service.js

@@ -31,19 +31,17 @@ const MEMORY_COMMANDS = [
 
 const MEMORY_AREAS = [
   { key: storageAccessProtocol.AREA.ADDR32, label: 'addr32', name: 'ADDR32', addressWidth: 32 },
+  { key: storageAccessProtocol.AREA.CODEINFO, label: 'codeinfo', name: 'CODEINFO', readOnly: true },
   { key: storageAccessProtocol.AREA.DATA, label: 'data', name: 'DATA' },
   { key: storageAccessProtocol.AREA.IDATA, label: 'idata', name: 'IDATA' },
   { key: storageAccessProtocol.AREA.XDATA, label: 'xdata', name: 'XDATA' },
-  { key: storageAccessProtocol.AREA.CODE, label: 'code', name: 'CODE' }
+  { key: storageAccessProtocol.AREA.CODE, label: 'code', name: 'CODE', readOnly: true }
 ]
 const MEMORY_READ_INDEX = 0
 const MEMORY_WRITE_INDEX = 1
 
 const CONTROL_COMMANDS = [
-  { key: 'reset', label: '复位', op: storageAccessProtocol.CONTROL_OP.RESET },
-  { key: 'start', label: '启动', op: storageAccessProtocol.CONTROL_OP.START },
-  { key: 'stop', label: '停止', op: storageAccessProtocol.CONTROL_OP.STOP },
-  { key: 'controlRef', label: '控制参考值', op: storageAccessProtocol.CONTROL_OP.SET_CONTROL_REF, hidden: true }
+  { key: 'reset', label: '复位', op: storageAccessProtocol.CONTROL_OP.RESET }
 ]
 
 let syncedDeviceCaps = {
@@ -173,25 +171,35 @@ async function readCodeInfoBlock(label, kind, options = {}) {
     ...options,
     useDeviceCaps: false
   })
-  const descriptorResponse = await executeControl(
-    storageAccessProtocol.CONTROL_OP.READ_CODE_INFO_DESCRIPTOR,
-    [],
+  const descriptorBytes = await readMemory(
+    storageAccessProtocol.AREA.CODEINFO,
+    storageAccessProtocol.CODE_INFO_DESCRIPTOR_ADDRESS,
+    storageAccessProtocol.CODE_INFO_DESCRIPTOR_BYTE_LENGTH,
     label,
     `${kind}-descriptor`,
     {
       ...transferOptions,
-      expectedByteLength: storageAccessProtocol.CODE_INFO_DESCRIPTOR_BYTE_LENGTH,
       useDeviceCaps: false,
       showModal: false
     }
   )
-  if (!descriptorResponse) {
+  if (!descriptorBytes) {
     if (transferOptions.showModal !== false) {
       transport.showCommandAlert(label, 'CodeInfo 描述符读取失败或超时')
     }
     return null
   }
 
+  let descriptorResponse
+  try {
+    descriptorResponse = storageAccessProtocol.parseCodeInfoDescriptorBytes(descriptorBytes)
+  } catch (error) {
+    if (transferOptions.showModal !== false) {
+      transport.showCommandAlert(label, error.message || 'CodeInfo 描述符长度无效')
+    }
+    return null
+  }
+
   const codeInfoAddress = Number(descriptorResponse.codeInfoAddress || 0)
   const codeInfoByteLength = Number(descriptorResponse.codeInfoByteLength || 0)
   let codeInfoAddressWidth
@@ -205,15 +213,7 @@ async function readCodeInfoBlock(label, kind, options = {}) {
     }
     return null
   }
-
-  const codeInfoMemoryEndian = String(descriptorResponse.codeInfoMemoryEndian || '').trim()
-  if (!codeInfoMemoryEndian) {
-    if (transferOptions.showModal !== false) {
-      transport.showCommandAlert(label, 'CodeInfo 描述符内存字节序标记无效')
-    }
-    return null
-  }
-
+  const codeInfoMemoryEndian = String(descriptorResponse.codeInfoMemoryEndian || 'big').trim()
   const codeInfoMaxPacketLength = Number(descriptorResponse.codeInfoMaxPacketLength || 0) & 0xFFFF
   const codeInfoArea = codeInfoAddressWidth === 32 ? storageAccessProtocol.AREA.ADDR32 : storageAccessProtocol.AREA.CODE
   const codeInfoReadAddress = codeInfoAddressWidth === 32 ? codeInfoAddress : (codeInfoAddress & 0xFFFF)
@@ -247,9 +247,7 @@ async function readCodeInfoBlock(label, kind, options = {}) {
     codeInfoAddressWidth,
     codeInfoByteLength,
     codeInfoBytes,
-    codeInfoDescriptorBytes: Array.isArray(descriptorResponse.codeInfoDescriptorBytes)
-      ? descriptorResponse.codeInfoDescriptorBytes
-      : [],
+    codeInfoDescriptorBytes: descriptorBytes,
     codeInfoMaxPacketLength,
     codeInfoMemoryEndian,
     codeInfoMemoryEndianMark: Number(descriptorResponse.codeInfoMemoryEndianMark || 0) & 0xFFFF,
@@ -303,7 +301,7 @@ function resolveMemoryArea(index, commandKey = 'read') {
   const memoryAreas = getMemoryAreaOptions()
   let areaIndex = Number(index) || 0
 
-  if (commandKey === 'write' && memoryAreas[areaIndex] && memoryAreas[areaIndex].key === storageAccessProtocol.AREA.CODE) {
+  if (commandKey === 'write' && memoryAreas[areaIndex] && memoryAreas[areaIndex].readOnly) {
     areaIndex = 0
   }
   if (!memoryAreas[areaIndex]) areaIndex = 0
@@ -355,9 +353,14 @@ function normalizeMemoryCommandState(current = {}, changed = {}) {
   const resolvedArea = resolveMemoryArea(next.storageAccessAreaIndex, command.key)
   const areaIndex = resolvedArea.index
   const area = resolvedArea.area
+  const isCodeInfoDescriptor = command.key === 'read' && area.key === storageAccessProtocol.AREA.CODEINFO
   const addressWidth = getAreaAddressWidth(area)
-  const addressText = normalizeDisplayHexText(next.storageAccessAddress, addressWidth)
-  const lengthText = normalizeDisplayHexText(next.storageAccessLength, 16)
+  const addressText = isCodeInfoDescriptor
+    ? '0000'
+    : normalizeDisplayHexText(next.storageAccessAddress, addressWidth)
+  const lengthText = isCodeInfoDescriptor
+    ? storageAccessProtocol.CODE_INFO_DESCRIPTOR_BYTE_LENGTH.toString(16).toUpperCase().padStart(4, '0')
+    : normalizeDisplayHexText(next.storageAccessLength, 16)
   const dataText = normalizeHexText(next.storageAccessDataText)
   const dataBytes = dataText ? parseHexBytes(dataText) : []
   const address = parseInt(addressText || '0', 16)
@@ -382,8 +385,8 @@ function normalizeMemoryCommandState(current = {}, changed = {}) {
     const hexError = validateHexText(next.storageAccessDataText)
     if (hexError) {
       errorText = hexError
-    } else if (area.key === storageAccessProtocol.AREA.CODE) {
-      errorText = 'code 区暂不支持写入'
+    } else if (area.readOnly) {
+      errorText = `${area.label} 区不可写`
     } else if (dataBytes.length !== byteLength) {
       errorText = `写入长度为 ${byteLength} 字节,当前数据为 ${dataBytes.length} 字节`
     }
@@ -409,7 +412,9 @@ function normalizeMemoryCommandState(current = {}, changed = {}) {
     storageAccessLength: lengthText,
     storageAccessPreviewHexText: previewHex,
     storageAccessPreviewText: canPreview
-      ? `${area.label} 0x${addressWidth === 32 ? formatDwordHex(address) : address.toString(16).toUpperCase().padStart(4, '0')} / ${byteLength} bytes`
+      ? (isCodeInfoDescriptor
+        ? 'codeinfo descriptor'
+        : `${area.label} 0x${addressWidth === 32 ? formatDwordHex(address) : address.toString(16).toUpperCase().padStart(4, '0')} / ${byteLength} bytes`)
       : '',
     storageAccessShowWriteData: command.key === 'write',
     storageAccessTitleText: `${command.label}命令`
@@ -490,92 +495,18 @@ async function executeMemoryCommand(data = {}, options = {}) {
   }
 }
 
-function normalizeReferenceText(value, fallback = '0') {
-  const source = String(value === undefined || value === null ? '' : value).trim()
-  if (!source) return fallback
-
-  const hexText = source.replace(/^0x/i, '')
-  if (/^[0-9A-F]{1,4}$/i.test(hexText) && /[A-F]/i.test(hexText)) {
-    const rawValue = parseInt(hexText, 16) || 0
-    return String(rawValue & 0x8000 ? rawValue - 0x10000 : rawValue)
-  }
-
-  if (/^-?\d+$/.test(source)) {
-    return String(Number(source))
-  }
-
-  return source
-}
-
-function normalizeControlReference(value) {
-  const text = String(value === undefined || value === null ? '' : value).trim()
-  if (!text) {
-    return {
-      bytes: [],
-      errorText: '控制参考值请输入十进制',
-      text,
-      value: 0
-    }
-  }
-  if (!/^-?\d+$/.test(text)) {
-    return {
-      bytes: [],
-      errorText: '控制参考值只支持十进制',
-      text,
-      value: 0
-    }
-  }
-
-  const valueNumber = Number(text)
-  if (valueNumber < -0x8000 || valueNumber > 0x7FFF) {
-    return {
-      bytes: [],
-      errorText: '控制参考值范围为 -32768 - 32767',
-      text,
-      value: 0
-    }
-  }
-
-  const wordValue = valueNumber & 0xFFFF
-
-  return {
-    bytes: [
-      (wordValue >>> 8) & 0xFF,
-      wordValue & 0xFF
-    ],
-    errorText: '',
-    text,
-    value: valueNumber
-  }
-}
-
 function getControlCommand(commandKey) {
   return CONTROL_COMMANDS.find((command) => command.key === commandKey) || null
 }
 
-function normalizeControlState(current = {}, changed = {}) {
-  const next = {
-    storageAccessControlRefText: current.storageAccessControlRefText || '0',
-    ...changed
-  }
-  const controlRefText = normalizeReferenceText(next.storageAccessControlRefText, '0')
-  const controlRef = normalizeControlReference(controlRefText)
-
-  return {
-    storageAccessControlRefErrorText: controlRef.errorText,
-    storageAccessControlRefPreviewHexText: controlRef.errorText
-      ? ''
-      : bytesToHex(storageAccessProtocol.buildControlReferenceFrame(controlRef.value), ' '),
-    storageAccessControlRefText: controlRefText
-  }
+function normalizeControlState() {
+  return {}
 }
 
 function getControlCommands() {
   return CONTROL_COMMANDS.map((command) => ({
     ...command,
-    previewHexText: command.key === 'controlRef'
-      ? ''
-      : bytesToHex(storageAccessProtocol.buildControlFrame(command.op), ' ')
+    previewHexText: bytesToHex(storageAccessProtocol.buildControlFrame(command.op), ' ')
   }))
 }
 
@@ -636,19 +567,9 @@ async function executeControlCommand(commandKey, data = {}, options = {}) {
     }
   }
 
-  const controlState = normalizeControlState(data)
-  if (command.key === 'controlRef' && controlState.storageAccessControlRefErrorText) {
-    return {
-      errorText: controlState.storageAccessControlRefErrorText,
-      ok: false
-    }
-  }
-
   const response = await executeControl(
     command.op,
-    command.key === 'controlRef'
-      ? normalizeControlReference(controlState.storageAccessControlRefText).bytes
-      : [],
+    [],
     command.label || '特殊指令',
     `storage-control-${command.key}`,
     {
@@ -664,14 +585,6 @@ async function executeControlCommand(commandKey, data = {}, options = {}) {
     }
   }
 
-  if (response.controlStatus !== 0) {
-    return {
-      errorText: response.controlStatusText || '设备拒绝执行',
-      ok: false,
-      response
-    }
-  }
-
   return {
     command,
     ok: true,

+ 0 - 28
pages/communication/communication.js

@@ -306,34 +306,6 @@ Page({
     this.setData(nextState)
   },
 
-  onStorageAccessControlRefInput(event) {
-    this.setData(normalizeStorageAccessSpecialState(this.data, {
-      storageAccessControlRefText: event.detail.value
-    }))
-  },
-
-  async onStorageAccessControlRefConfirm() {
-    const controlRefText = String(this.data.storageAccessControlRefText || '').trim()
-    const now = Date.now()
-    if (this.lastStorageAccessControlRefSent
-      && this.lastStorageAccessControlRefSent.text === controlRefText
-      && now - this.lastStorageAccessControlRefSent.time < 600) {
-      return
-    }
-    this.lastStorageAccessControlRefSent = {
-      text: controlRefText,
-      time: now
-    }
-
-    await this.sendStorageAccessSpecialCommand({
-      currentTarget: {
-        dataset: {
-          command: 'controlRef'
-        }
-      }
-    })
-  },
-
   onStorageAccessAreaChange(event) {
     const storageAccessAreaIndex = Number(event.detail.value)
     const nextState = normalizeStorageAccessState(this.data, {

+ 0 - 18
pages/communication/communication.wxml

@@ -141,7 +141,6 @@
         </view>
         <view class="storage-special-section">
           <view class="storage-special-section-title">特殊指令</view>
-          <view class="generated-meta">特殊指令不带地址和长度,仅携带 OP 与必要数据。</view>
         </view>
         <view class="storage-special-actions">
           <view
@@ -155,23 +154,6 @@
             {{item.label}}
           </view>
         </view>
-        <view class="protocol-row">
-          <view class="protocol-label">控制参考值</view>
-          <input
-            class="protocol-input protocol-row-input"
-            maxlength="6"
-            value="{{storageAccessControlRefText}}"
-            bindinput="onStorageAccessControlRefInput"
-            bindblur="onStorageAccessControlRefConfirm"
-            bindconfirm="onStorageAccessControlRefConfirm"
-            confirm-type="send"
-            placeholder="1500"
-          />
-        </view>
-        <view class="generated-frame">
-          <view class="generated-title">控制参考值</view>
-          <view class="generated-value">{{storageAccessControlRefPreviewHexText || '--'}}</view>
-        </view>
       </view>
     </view>
 

+ 4 - 0
pages/params/params.wxml

@@ -268,6 +268,10 @@
             <view class="generic-info-label">来源</view>
             <view class="generic-info-value">{{parameterDialog.sourceMetaText}}</view>
           </view>
+          <view wx:if="{{parameterDialog.sourceByteLength}}" class="generic-info-row">
+            <view class="generic-info-label">TLV长度</view>
+            <view class="generic-info-value">{{parameterDialog.sourceByteLength}}B</view>
+          </view>
           <view wx:if="{{parameterDialog.showDataType}}" class="generic-info-row">
             <view class="generic-info-label">类型</view>
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.dataTypeText}}</view>

+ 143 - 77
protocols/storage-access/index.js

@@ -16,15 +16,17 @@ const PROTOCOL_NAME = 'storage-access'
 
 const CMD_ERR_MASK = 0x80
 const CMD_CONTROL_FLAG = 0x40
+const CMD_SPECIAL_OP_MASK = 0x3F
 const CMD_WRITE_MASK = 0x08
 const CMD_ADDRESS_MODE_MASK = 0x07
 const CMD_RESERVED_MASK = 0x30
-const CMD_CONTROL = 0x4F
-const CONTROL_RESPONSE_HEADER_LENGTH = 3
+const CMD_CONTROL = CMD_CONTROL_FLAG
+const CONTROL_RESPONSE_HEADER_LENGTH = 1
 const ADDRESS16_BYTE_LENGTH = 2
 const ADDRESS32_BYTE_LENGTH = 4
 
 const AREA = {
+  CODEINFO: 0x00,
   ADDR32: 0x07,
   DATA: 0x01,
   IDATA: 0x02,
@@ -33,22 +35,11 @@ const AREA = {
 }
 
 const CONTROL_OP = {
-  RESET: 0x01,
-  START: 0x02,
-  STOP: 0x03,
-  SET_CONTROL_REF: 0x04,
-  READ_CODE_INFO_DESCRIPTOR: 0x05
-}
-
-const CONTROL_STATUS_MESSAGES = {
-  0x00: '成功',
-  0x01: '不支持的指令',
-  0x02: '参数无效',
-  0x03: '设备忙',
-  0x04: '执行失败'
+  RESET: 0x01
 }
 
 const AREA_NAMES = {
+  [AREA.CODEINFO]: 'CODEINFO',
   [AREA.ADDR32]: 'ADDR32',
   [AREA.DATA]: 'DATA',
   [AREA.IDATA]: 'IDATA',
@@ -57,6 +48,8 @@ const AREA_NAMES = {
 }
 
 const AREA_BY_NAME = {
+  CODEINFO: AREA.CODEINFO,
+  CODE_INFO: AREA.CODEINFO,
   ADDR32: AREA.ADDR32,
   ADDRESS32: AREA.ADDR32,
   DATA: AREA.DATA,
@@ -95,7 +88,9 @@ const READ_RESPONSE_OVERHEAD_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2
 const WRITE_RESPONSE_LENGTH_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2
 const WRITE_RESPONSE_LENGTH_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2
 const EXCEPTION_RESPONSE_LENGTH = 4
-const CODE_INFO_DESCRIPTOR_BYTE_LENGTH = 11
+const CODE_INFO_DESCRIPTOR_ADDRESS = 0
+const CODE_INFO_DESCRIPTOR_BYTE_LENGTH = 9
+const CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH = 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH + 2
 const MEMORY_ENDIAN_MARK_BIG = 0x55AA
 const MEMORY_ENDIAN_MARK_LITTLE = 0xAA55
 const MEMORY_ENDIAN = {
@@ -106,9 +101,10 @@ const MEMORY_ENDIAN = {
 const STORAGE_CRC_OPTIONS = {
   byteOrder: BYTE_ORDER_HIGH
 }
-const VALID_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
-const MEMORY_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
-const RESERVED_AREAS = [0x00, 0x05, 0x06]
+const VALID_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
+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]
 
 function toByte(value, label) {
   if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
@@ -126,14 +122,6 @@ function toWord(value, label) {
   return value
 }
 
-function toInt16Word(value, label) {
-  if (!Number.isInteger(value) || value < -0x8000 || value > 0x7FFF) {
-    throw new Error(`${label}必须在 -32768 至 32767 之间`)
-  }
-
-  return value & 0xFFFF
-}
-
 function toUint32(value, label) {
   if (!Number.isInteger(value) || value < 0 || value > MAX_UINT32) {
     throw new Error(`${label}必须在 0x00000000 至 0xFFFFFFFF 之间`)
@@ -150,7 +138,7 @@ function normalizeArea(value) {
 
   const area = toByte(Number(value), '存储区域')
   if (VALID_AREAS.indexOf(area) < 0) {
-    throw new Error('存储区域必须为 addr32/data/idata/xdata/code')
+    throw new Error('存储区域必须为 codeinfo/data/idata/xdata/code/addr32')
   }
 
   return area
@@ -169,6 +157,8 @@ function getMemoryHeaderLength(area) {
 }
 
 function getReadRequestLength(area) {
+  if (Number(area) === AREA.CODEINFO) return 3
+
   return getMemoryHeaderLength(area) + 2
 }
 
@@ -177,6 +167,8 @@ function getWriteRequestOverhead(area) {
 }
 
 function getReadResponseOverhead(area) {
+  if (Number(area) === AREA.CODEINFO) return 1 + 2
+
   return getMemoryHeaderLength(area) + 2
 }
 
@@ -184,10 +176,14 @@ function getWriteResponseLength(area) {
   return getMemoryHeaderLength(area) + 2
 }
 
+function isCodeInfoDescriptorArea(area) {
+  return Number(area) === AREA.CODEINFO
+}
+
 function normalizeMemoryArea(value) {
   const area = normalizeArea(value)
   if (MEMORY_AREAS.indexOf(area) < 0) {
-    throw new Error('存储读写区域必须为 addr32/data/idata/xdata/code')
+    throw new Error('存储访问区域必须为 codeinfo/data/idata/xdata/code/addr32')
   }
 
   return area
@@ -294,19 +290,32 @@ function getMaxWriteByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, area = A
 }
 
 function buildCommand(area, isWrite = false) {
-  const normalizedArea = isWrite ? normalizeMemoryArea(area) : normalizeArea(area)
+  const normalizedArea = normalizeMemoryArea(area)
 
   if (RESERVED_AREAS.indexOf(normalizedArea) >= 0) {
-    throw new Error('存储访问区域号 0x00/0x05/0x06 暂时保留')
+    throw new Error('存储访问区域号 0x05/0x06 暂时保留')
+  }
+  if (isWrite && WRITABLE_AREAS.indexOf(normalizedArea) < 0) {
+    throw new Error(`${AREA_NAMES[normalizedArea] || '该'} 区不可写`)
   }
 
   return (isWrite ? CMD_WRITE_MASK : 0x00) | normalizedArea
 }
 
+function buildSpecialCommand(operation) {
+  const op = toByte(Number(operation), '特殊指令') & CMD_SPECIAL_OP_MASK
+  if (op <= 0) {
+    throw new Error('特殊指令必须在 0x01 至 0x3F 之间')
+  }
+
+  return CMD_CONTROL_FLAG | op
+}
+
 function decodeCommand(command) {
   const cmd = toByte(Number(command), '命令字')
   const sourceCommand = cmd & ~CMD_ERR_MASK
-  const isControl = sourceCommand === CMD_CONTROL
+  const isControl = !!(sourceCommand & CMD_CONTROL_FLAG)
+  const operation = isControl ? (sourceCommand & CMD_SPECIAL_OP_MASK) : 0
   const area = isControl ? CMD_CONTROL : (sourceCommand & CMD_ADDRESS_MODE_MASK)
   const reservedBits = sourceCommand & CMD_RESERVED_MASK
 
@@ -315,10 +324,12 @@ function decodeCommand(command) {
     area,
     command: cmd,
     hasError: !!(cmd & CMD_ERR_MASK),
+    hasInvalidSpecialOperation: isControl && operation === 0,
     hasReservedBits: !isControl && reservedBits !== 0,
     isAddress32: !isControl && isAddress32Area(area),
     isControl,
     isWrite: !isControl && !!(sourceCommand & CMD_WRITE_MASK),
+    operation,
     sourceCommand
   }
 }
@@ -339,6 +350,10 @@ function appendStorageCrc(bytes) {
 
 function buildReadFrame(area, address, byteLength, options = {}) {
   const normalizedArea = normalizeMemoryArea(area)
+  if (isCodeInfoDescriptorArea(normalizedArea)) {
+    return buildCodeInfoDescriptorFrame()
+  }
+
   const addressBytes = getAddressFieldByteLength(normalizedArea)
   const startAddress = addressBytes === ADDRESS32_BYTE_LENGTH
     ? toUint32(Number(address), '内存地址')
@@ -354,8 +369,8 @@ function buildReadFrame(area, address, byteLength, options = {}) {
 
 function buildWriteFrame(area, address, bytes, options = {}) {
   const normalizedArea = normalizeMemoryArea(area)
-  if (normalizedArea === AREA.CODE) {
-    throw new Error('code 区暂不支持写入')
+  if (WRITABLE_AREAS.indexOf(normalizedArea) < 0) {
+    throw new Error(`${AREA_NAMES[normalizedArea] || '该'} 区不可写`)
   }
 
   const addressBytes = getAddressFieldByteLength(normalizedArea)
@@ -373,18 +388,31 @@ function buildWriteFrame(area, address, bytes, options = {}) {
 }
 
 function buildControlFrame(operation, dataBytes = []) {
-  const op = toByte(Number(operation), '特殊指令')
+  const command = buildSpecialCommand(operation)
   const payload = toByteArray(dataBytes).map((byte) => toByte(Number(byte), '指令数据'))
 
-  return appendStorageCrc([CMD_CONTROL, op].concat(payload))
+  return appendStorageCrc([command].concat(payload))
 }
 
-function buildControlReferenceFrame(referenceValue) {
-  return buildControlFrame(CONTROL_OP.SET_CONTROL_REF, splitWord(toInt16Word(Number(referenceValue), '控制参考值')))
+function buildCodeInfoDescriptorFrame() {
+  return appendStorageCrc([buildCommand(AREA.CODEINFO, false)])
 }
 
-function buildCodeInfoDescriptorFrame() {
-  return buildControlFrame(CONTROL_OP.READ_CODE_INFO_DESCRIPTOR)
+function parseCodeInfoDescriptorBytes(bytes) {
+  const dataBytes = toByteArray(bytes)
+  if (dataBytes.length < CODE_INFO_DESCRIPTOR_BYTE_LENGTH) {
+    throw new Error('CodeInfo 描述符长度无效')
+  }
+
+  return {
+    codeInfoAddress: readDword(dataBytes, 0),
+    codeInfoAddressWidth: dataBytes[6] & 0xFF,
+    codeInfoByteLength: readWord(dataBytes, 4),
+    codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH),
+    codeInfoMaxPacketLength: readWord(dataBytes, 7),
+    codeInfoMemoryEndian: MEMORY_ENDIAN.BIG,
+    codeInfoMemoryEndianMark: 0
+  }
 }
 
 function formatHex(bytes) {
@@ -393,7 +421,7 @@ function formatHex(bytes) {
 
 function parseStorageAccessResponse(bytes) {
   const frame = toByteArray(bytes)
-  if (frame.length < EXCEPTION_RESPONSE_LENGTH || !hasValidStorageCrc(frame)) return null
+  if (frame.length < 3 || !hasValidStorageCrc(frame)) return null
 
   const command = frame[0] & 0xFF
   const decoded = decodeCommand(command)
@@ -416,6 +444,32 @@ function parseStorageAccessResponse(bytes) {
 
   if (decoded.hasReservedBits) return null
   if (!AREA_NAMES[decoded.area]) return null
+
+  if (isCodeInfoDescriptorArea(decoded.area)) {
+    if (decoded.isWrite || frame.length !== CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH) return null
+
+    const dataBytes = frame.slice(1, 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH)
+    const response = {
+      address: CODE_INFO_DESCRIPTOR_ADDRESS,
+      addressWidth: 0,
+      area: decoded.area,
+      areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
+      byteLength: CODE_INFO_DESCRIPTOR_BYTE_LENGTH,
+      command,
+      dataBytes,
+      isException: false,
+      isAddress32: false,
+      isWrite: false,
+      protocol: PROTOCOL_NAME,
+      words: bytesToWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
+    }
+
+    return {
+      ...response,
+      ...parseCodeInfoDescriptorBytes(dataBytes)
+    }
+  }
+
   const addressBytes = decoded.addressBytes
   const headerLength = getMemoryHeaderLength(decoded.area)
 
@@ -447,7 +501,7 @@ function parseStorageAccessResponse(bytes) {
 
   const dataBytes = frame.slice(dataStart, dataEnd)
 
-  return {
+  const response = {
     address,
     addressWidth: decoded.isAddress32 ? 32 : 16,
     area: decoded.area,
@@ -461,9 +515,13 @@ function parseStorageAccessResponse(bytes) {
     protocol: PROTOCOL_NAME,
     words: bytesToWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
   }
+
+  return response
 }
 
 function parseStorageControlResponse(frame, decoded) {
+  if (decoded.hasInvalidSpecialOperation) return null
+
   if (decoded.hasError) {
     if (frame.length !== EXCEPTION_RESPONSE_LENGTH) return null
 
@@ -475,6 +533,7 @@ function parseStorageControlResponse(frame, decoded) {
       isControl: true,
       isException: true,
       isWrite: false,
+      operation: decoded.operation,
       protocol: PROTOCOL_NAME,
       sourceCommand: decoded.sourceCommand
     }
@@ -482,8 +541,6 @@ function parseStorageControlResponse(frame, decoded) {
 
   if (frame.length < CONTROL_RESPONSE_HEADER_LENGTH + 2) return null
 
-  const operation = frame[1] & 0xFF
-  const status = frame[2] & 0xFF
   const dataStart = CONTROL_RESPONSE_HEADER_LENGTH
   const dataEnd = frame.length - 2
   const byteLength = Math.max(0, dataEnd - dataStart)
@@ -495,44 +552,46 @@ function parseStorageControlResponse(frame, decoded) {
     areaName: 'CONTROL',
     byteLength,
     command: frame[0] & 0xFF,
-    controlStatus: status,
-    controlStatusText: CONTROL_STATUS_MESSAGES[status] || '未知状态',
     dataBytes,
     isControl: true,
     isException: false,
     isWrite: false,
-    operation,
-    protocol: PROTOCOL_NAME,
-    ...(operation === CONTROL_OP.READ_CODE_INFO_DESCRIPTOR && dataBytes.length >= CODE_INFO_DESCRIPTOR_BYTE_LENGTH
-      ? (() => {
-        const endian = parseMemoryEndianMark(dataBytes, 7)
-
-        return {
-          codeInfoAddress: readDword(dataBytes, 0),
-          codeInfoByteLength: readWord(dataBytes, 4),
-          codeInfoAddressWidth: dataBytes[6] & 0xFF,
-          codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH),
-          codeInfoMaxPacketLength: readWord(dataBytes, 9),
-          codeInfoMemoryEndian: endian.memoryEndian,
-          codeInfoMemoryEndianMark: endian.marker
-        }
-      })()
-      : {})
+    operation: decoded.operation,
+    protocol: PROTOCOL_NAME
   }
 }
 
 function parseStorageAccessRequest(bytes) {
   const frame = toByteArray(bytes)
-  if (frame.length < 4 || !hasValidStorageCrc(frame)) return null
+  if (frame.length < 3 || !hasValidStorageCrc(frame)) return null
 
   const command = frame[0] & 0xFF
 
   const decoded = decodeCommand(command)
-  if (decoded.isControl) return parseStorageControlRequest(frame)
-  if (frame.length < READ_REQUEST_LENGTH_16) return null
+  if (decoded.isControl) return parseStorageControlRequest(frame, decoded)
   if (decoded.hasError) return null
   if (decoded.hasReservedBits) return null
   if (!AREA_NAMES[decoded.area]) return null
+  if (isCodeInfoDescriptorArea(decoded.area)) {
+    if (decoded.isWrite || frame.length !== getReadRequestLength(decoded.area)) return null
+
+    return {
+      address: CODE_INFO_DESCRIPTOR_ADDRESS,
+      addressWidth: 0,
+      area: decoded.area,
+      areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
+      byteLength: CODE_INFO_DESCRIPTOR_BYTE_LENGTH,
+      command,
+      dataBytes: [],
+      isAddress32: false,
+      isWrite: false,
+      kind: 'raw-hex',
+      operation: 'read',
+      protocol: PROTOCOL_NAME,
+      quantity: CODE_INFO_DESCRIPTOR_BYTE_LENGTH
+    }
+  }
+  if (frame.length < READ_REQUEST_LENGTH_16) return null
 
   const addressBytes = decoded.addressBytes
   const headerLength = getMemoryHeaderLength(decoded.area)
@@ -563,11 +622,10 @@ function parseStorageAccessRequest(bytes) {
   }
 }
 
-function parseStorageControlRequest(frame) {
-  if (frame.length < 4) return null
+function parseStorageControlRequest(frame, decoded = decodeCommand(frame && frame[0])) {
+  if (frame.length < 3 || decoded.hasInvalidSpecialOperation) return null
 
-  const operation = frame[1] & 0xFF
-  const dataStart = 2
+  const dataStart = CONTROL_RESPONSE_HEADER_LENGTH
   const dataEnd = frame.length - 2
   const byteLength = Math.max(0, dataEnd - dataStart)
 
@@ -575,12 +633,12 @@ function parseStorageControlRequest(frame) {
     area: CMD_CONTROL,
     areaName: 'CONTROL',
     byteLength,
-    command: CMD_CONTROL,
+    command: frame[0] & 0xFF,
     dataBytes: frame.slice(dataStart, dataEnd),
     isControl: true,
     isWrite: false,
     kind: 'storage-control',
-    operation,
+    operation: decoded.operation,
     protocol: PROTOCOL_NAME
   }
 }
@@ -593,8 +651,6 @@ function getExpectedResponseLength(expected, responseCommand, responseBytes = []
 
   if (expected.isControl) {
     if (responseBytes.length < CONTROL_RESPONSE_HEADER_LENGTH) return 0
-    const status = responseBytes[2] & 0xFF
-    if (status !== 0) return CONTROL_RESPONSE_HEADER_LENGTH + 2
 
     return CONTROL_RESPONSE_HEADER_LENGTH + Number(expected.expectedByteLength || 0) + 2
   }
@@ -603,6 +659,10 @@ function getExpectedResponseLength(expected, responseCommand, responseBytes = []
     return getWriteResponseLength(expected.area)
   }
 
+  if (isCodeInfoDescriptorArea(expected.area)) {
+    return CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH
+  }
+
   const headerLength = getMemoryHeaderLength(expected.area)
   if (responseBytes.length < headerLength) return 0
 
@@ -647,6 +707,9 @@ function getReadBufferHint(expected) {
   if (expected.operation === 'write' || expected.isWrite) {
     return getWriteResponseLength(expected.area)
   }
+  if (isCodeInfoDescriptorArea(expected.area)) {
+    return CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH
+  }
 
   return getReadResponseOverhead(expected.area) + Number(expected.byteLength || expected.quantity || 0)
 }
@@ -763,11 +826,12 @@ function createExpected(area, address, byteLength, isWrite, kind, options = {})
 
 function createControlExpected(operation, kind = 'storage-control', options = {}) {
   const op = toByte(Number(operation), '特殊指令')
+  const command = buildSpecialCommand(op)
 
   return {
     area: CMD_CONTROL,
     byteLength: 0,
-    command: CMD_CONTROL,
+    command,
     expectedByteLength: Number(options.expectedByteLength) || 0,
     isControl: true,
     isWrite: false,
@@ -851,16 +915,17 @@ module.exports = {
   ADDRESS32_BYTE_LENGTH,
   CMD_CONTROL,
   CMD_CONTROL_FLAG,
+  CMD_SPECIAL_OP_MASK,
   CMD_ADDRESS_MODE_MASK,
   CMD_ERR_MASK,
   CMD_RESERVED_MASK,
   CMD_WRITE_MASK,
   CONTROL_OP,
   CONTROL_RESPONSE_HEADER_LENGTH,
-  CONTROL_STATUS_MESSAGES,
   DEFAULT_MAX_FRAME_BYTES,
   EXCEPTION_MESSAGES,
   EXCEPTION_RESPONSE_LENGTH,
+  CODE_INFO_DESCRIPTOR_ADDRESS,
   CODE_INFO_DESCRIPTOR_BYTE_LENGTH,
   MAX_PAYLOAD_BYTES,
   MEMORY_ENDIAN,
@@ -880,9 +945,9 @@ module.exports = {
   appendStorageCrc,
   buildCommand,
   buildCodeInfoDescriptorFrame,
-  buildControlReferenceFrame,
   buildControlFrame,
   buildReadFrame,
+  buildSpecialCommand,
   buildWriteFrame,
   createControlExpected,
   createExpected,
@@ -898,6 +963,7 @@ module.exports = {
   normalizeArea,
   normalizeMaxFrameBytes,
   normalizeMemoryArea,
+  parseCodeInfoDescriptorBytes,
   resolveDescriptorMaxFrameBytes,
   response,
   splitDword,

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

@@ -99,12 +99,12 @@ BLE 透传链路
 
 - 普通内存读写使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
 - `LEN` 固定 16 位,单位始终为字节。
-- `CMD bit0~bit2` 区分地址模式或区域:`0x07` 为 32 位地址,`0x01..0x04` 为 DATA/IDATA/XDATA/CODE 的 16 位地址,`0x00/0x05/0x06` 保留。
-- `CMD bit3` 为读写位,`bit4/bit5` 暂时保留,`bit7` 为异常标志。
-- 普通区域包括 `DATA`、`IDATA`、`XDATA`、`CODE`,其中 `CODE` 只读。
-- 特殊指令使用 `CMD=0x4F`,用于复位、启动、停止、控制参考值和 CodeInfo 描述符读取
-- CodeInfo 同步只使用特殊指令 `OP=0x05` 获取 `CODE_ADDR32 + CODE_LEN16 + ADDR_WIDTH8 + MEMORY_ENDIAN16 + MAX_PACKET16`,再按 bootstrap 声明的地址宽度、目标内存变量字节序和包长读取完整信息块。
-- 协议控制字段始终固定大端;结构体字段和单独变量等目标内存值按 bootstrap `MEMORY_ENDIAN` 编解码。
+- `CMD bit0~bit2` 区分普通区域:`0x00` 为 CODEINFO 描述符,`0x01..0x04` 为 DATA/IDATA/XDATA/CODE 的 16 位地址,`0x07` 为 32 位地址,`0x05..0x06` 保留。
+- `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`,再按 `ADDR_WIDTH` 用 CODE 或 ADDR32 读取完整信息块。
+- 协议控制字段始终固定大端;结构体字段和单独变量等目标内存值当前默认按大端编解码。
 - 不直接调用 BLE 发送,不负责弹窗;发包编排由 `features/storage-access/service.js` 处理。
 
 完整帧格式和从机实现参考见 `存储访问协议.md`。
@@ -160,11 +160,11 @@ BLE 透传链路
 职责:
 
 - 解析 CodeInfo 纯 TLV 信息块,未知 TLV 类型跳过。
-- 使用同步 bootstrap 返回的 `CODE_LEN`、`ADDR_WIDTH`、`MEMORY_ENDIAN` 和 `MAX_PACKET`;`CODE_LEN` 决定 CodeInfo 总长度,`ADDR_WIDTH` 只决定读取 CodeInfo 信息块本体时的地址宽度,内存入口地址宽度由 TLV `TYPE` 自描述,`MEMORY_ENDIAN` 进入参数组上下文用于变量值编解码
+- 使用 CODEINFO 描述符返回的 `TLV_LEN` 决定 CodeInfo 总长度,并使用 `ADDR_WIDTH/MAX_PACKET` 作为同步上下文;内存入口地址宽度和区域由 TLV `TYPE` 自描述
 - 解析 UTF-8/ASCII 电机型号、芯片型号和转换相关可选 TLV 参数。
-- 按 `mem_type` 映射真实存储区域
-- 按 TLV `TYPE=0x20/0x21/0x28/0x29` 区分 16/32 位地址结构体实例和单独变量
-- 生成参数组初始结构,结构体未导入定义时按字节占位,单独变量按 1/2/4 字节推断为 `uint8_t`、`uint16_t`、`uint32_t`
+- 固定 TLV `0x01~0x08` 映射真实存储区域、地址宽度和结构体/变量类型,并按结构体、单独变量成对排列;VALUE 固定为 `addr(2/4) + byte_len16 + name[32]`
+- `0x20~0x3F` 为自定义 TLV,板卡参数从 `0x40` 开始递增
+- 生成参数组初始结构,结构体未导入定义时按字节占位,单独变量按 TLV `byte_len` 显示为未配置原始字节,后续由 UI 或 enum 导入确定同长度的解释类型
 
 ## 6. 功能服务
 
@@ -191,9 +191,9 @@ BLE 透传链路
 
 - 包装 `protocols/storage-access/index.js` 的读写、特殊指令和 CodeInfo 同步能力。
 - 根据设置的最大包长决定分片长度。
-- 执行 `OP=0x05` 描述符读取,再用 `MODE=0x07` 或 `MODE=0x04 CODE` 完整读取 CodeInfo。
+- 读取 `area=0x00 CODEINFO` 描述符,再按 `ADDR_WIDTH` 用 `area=0x04 CODE` 或 `area=0x07 ADDR32` 完整读取 CodeInfo。
 - 将解析结果转换为参数组。
-- 提供复位、启动、停止、控制参考值等特殊指令。
+- 提供复位特殊指令。
 
 ### 6.3 标准 Modbus 服务
 
@@ -272,9 +272,9 @@ BLE 透传链路
   -> features/parameter-groups/service.syncFromStorageAccessCodeInfo
   -> features/storage-access/service.syncCodeInfo
   -> features/storage-access/service.readCodeInfoBlock
-  -> protocols/storage-access/index.js 构建特殊指令和普通读帧
-  -> 特殊指令 OP=0x05 读取 CODE_ADDR32 + CODE_LEN16 + ADDR_WIDTH8 + MEMORY_ENDIAN16 + MAX_PACKET16
-  -> 按 ADDR_WIDTH/MAX_PACKET 分片读取 CodeInfo TLV 信息块,保存 MEMORY_ENDIAN 到参数组上下文
+  -> protocols/storage-access/index.js 构建普通读帧
+  -> 发送 00 + CRC 读取 area=0x00 CODEINFO 描述符,获得 TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16
+  -> 按描述符和设置页最大包长分片读取 CodeInfo TLV 信息块
   -> domain/storage-access/code-info-parser.parseCodeInfo
   -> domain/storage-access/code-info-parser.createGroupsFromCodeInfo
   -> features/parameter-groups/imports.mergeImportedGroups

+ 115 - 126
存储访问协议.md

@@ -8,12 +8,12 @@
 
 - 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
 - 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
-- 复位、启动、停止、控制参考值、CodeInfo 描述符读取使用特殊指令帧。
-- CodeInfo 同步只使用特殊指令 `OP=0x05`,不保留旧同步区域入口
+- 复位使用 `bit6=1` 的特殊指令帧;当前仅定义复位特殊指令
+- CodeInfo 同步先读取普通区域 `area=0x00` 的 codeinfo 描述符,再用 `addr32` 读取 TLV 信息块
 
 ## 2. 字节序与 CRC
 
-除 CRC 算法内部计算外,所有多字节协议控制字段均为大端序。目标内存变量的多字节值不跟随协议控制字段字节序,而是按第一次同步 bootstrap 描述符中的 `MEMORY_ENDIAN` 解释和编辑。
+除 CRC 算法内部计算外,所有多字节协议控制字段均为大端序。当前目标内存变量的多字节值默认按大端解释和编辑。
 
 ```text
 32 位地址 : ADDR_3 ADDR_2 ADDR_1 ADDR_0
@@ -39,19 +39,19 @@ CRC 覆盖除最后两个 CRC 字节外的整帧内容。
 每帧第 1 字节为 `CMD`。
 
 ```text
-bit7      ERR       异常标志
-bit6      CTL       特殊指令标志位
+bit7      ERR       故障标志,仅回帧出现
+bit6      SPECIAL   特殊指令标志位
 bit5~bit4 RSV       保留,普通读写保持 0
-bit3      RW        读写标志,0=读,1=写
-bit2~bit0 MODE      地址模式或存储区域
+bit3      RW        普通读写标志,0=读,1=写
+bit2~bit0 AREA      普通读写区域码
 ```
 
 | 位 | 值 | 含义 |
 |---|---:|---|
-| ERR | 0 | 正常请求或正常响应 |
-| ERR | 1 | 异常响应 |
-| CTL | 0 | 普通内存读写 |
-| CTL | 1 | 与 `bit3=1` 且 `MODE=0x7` 组合为特殊指令,即 `CMD=0x4F` |
+| ERR | 0 | 请求或正常响应 |
+| ERR | 1 | 故障响应 |
+| SPECIAL | 0 | 普通存储访问 |
+| SPECIAL | 1 | 特殊指令,`bit0~bit5` 表示特殊指令码 |
 | RSV | 0 | 当前版本固定为 0 |
 | RW | 0 | 读操作 |
 | RW | 1 | 写操作 |
@@ -64,21 +64,28 @@ bit2~bit0 MODE      地址模式或存储区域
 异常响应 : CMD_ERR = CMD | 0x80
 ```
 
-`CMD=0x4F` 固定保留为特殊指令帧,不作为普通存储区域。
+特殊指令生成规则:
+
+```text
+特殊请求 : CMD = 0x40 | SPECIAL_CODE
+异常响应 : CMD_ERR = CMD | 0x80
+```
+
+当前仅定义 `SPECIAL_CODE=0x01` 表示复位,因此复位请求 `CMD=0x41`,复位异常响应 `CMD=0xC1`。
 
 ## 4. AREA 定义
 
 | AREA | 名称 | 读 | 写 | 用途 |
 |---:|---|---|---|---|
-| `0x00` | 保留 | 禁止 | 禁止 | 保留 |
+| `0x00` | CODEINFO | 支持 | 禁止 | 同步时读取一次,返回 TLV 起始地址与长度 |
 | `0x01` | DATA | 支持 | 支持 | 内部直接寻址 RAM 区 |
 | `0x02` | IDATA | 支持 | 支持 | 内部间接寻址 RAM 区 |
 | `0x03` | XDATA | 支持 | 支持 | 外部数据空间或扩展 RAM |
-| `0x04` | CODE | 支持 | 禁止 | 程序存储区 |
+| `0x04` | CODE | 支持 | 禁止 | 程序存储区,16 位地址 |
 | `0x05..0x06` | 保留 | 禁止 | 禁止 | 保留 |
 | `0x07` | ADDR32 | 支持 | 支持 | 32 位统一地址模式 |
 
-写 `CODE` 区必须返回写保护异常。
+写 `CODEINFO` 或 `CODE` 区必须返回写保护异常。
 
 ## 5. 普通内存访问帧
 
@@ -115,7 +122,7 @@ MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 
 ```text
 MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
-MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
+MODE=0x01..0x03: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
 ```
 
 长度分别为 9 字节或 7 字节。
@@ -128,37 +135,23 @@ MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
 CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 ```
 
-长度固定 4 字节。`CMD_ERR` 为原请求 `CMD | 0x80`。特殊指令异常响应使用 `0xCF`。
+长度固定 4 字节。`CMD_ERR` 为原请求 `CMD | 0x80`。
 
 ## 6. 特殊指令帧
 
-特殊指令使用固定 `CMD=0x4F`
+特殊指令使用 `CMD bit6=1`,`bit0~bit5` 表示特殊指令码。当前仅定义 `0x01` 复位
 
 ```text
-请求: 4F OP DATA... CRC_H CRC_L
-响应: 4F OP STATUS DATA... CRC_H CRC_L
-异常: CF EXCEPTION_CODE CRC_H CRC_L
+请求: CMD DATA... CRC_H CRC_L
+响应: CMD DATA... CRC_H CRC_L
+异常: CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 ```
 
-`STATUS=0x00` 表示指令执行成功;非 0 表示从机识别了指令,但拒绝执行或执行失败。
-
-| OP | 名称 | 请求 DATA | 成功响应 DATA | 说明 |
-|---:|---|---|---|---|
-| `0x01` | RESET | 无 | 无 | 复位 |
-| `0x02` | START | 无 | 无 | 启动 |
-| `0x03` | STOP | 无 | 无 | 停止 |
-| `0x04` | SET_CONTROL_REF | `REF_H REF_L` | 无 | 控制参考值,`int16_t`,大端序 |
-| `0x05` | READ_CODE_INFO_DESCRIPTOR | 无 | `CODE_ADDR32 CODE_LEN16 ADDR_WIDTH8 MEMORY_ENDIAN16 MAX_PACKET16` | 读取 CodeInfo bootstrap 描述符 |
+特殊指令执行失败时直接返回异常帧,成功响应不额外携带状态字节。
 
-特殊指令状态码:
-
-| STATUS | 名称 | 说明 |
-|---:|---|---|
-| `0x00` | OK | 成功 |
-| `0x01` | UNSUPPORTED | 不支持的指令 |
-| `0x02` | INVALID_PARAM | 参数无效 |
-| `0x03` | BUSY | 设备忙 |
-| `0x04` | FAILED | 执行失败 |
+| 指令码 | CMD | 名称 | 请求 DATA | 成功响应 DATA | 说明 |
+|---:|---:|---|---|---|---|
+| `0x01` | `0x41` | RESET | 无 | 无 | 复位 |
 
 ## 7. 异常码
 
@@ -181,34 +174,33 @@ CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 
 CodeInfo 同步分两步:
 
-1. 使用特殊指令 `OP=0x05` 读取 bootstrap 描述符
-2. 按 bootstrap 返回的地址长度、目标内存字节序和最大包长读取完整 CodeInfo TLV 信息块。
+1. 读取普通区域 `area=0x00 CODEINFO` 的描述符,响应 DATA 为 9 字节
+2. 按描述符返回的 TLV 起始地址、`len16`、地址位宽和最大包长,读取完整 CodeInfo TLV 信息块。
 
-### 8.1 读取 bootstrap 描述符
+### 8.1 读取 CODEINFO 描述符
 
 请求:
 
 ```text
-4F 05 CRC_H CRC_L
+00 CRC_H CRC_L
 ```
 
 从机成功响应:
 
 ```text
-4F 05 00 CODE_ADDR_3 CODE_ADDR_2 CODE_ADDR_1 CODE_ADDR_0 CODE_LEN_H CODE_LEN_L ADDR_WIDTH ENDIAN_MARK_H ENDIAN_MARK_L MAX_PACKET_H MAX_PACKET_L CRC_H CRC_L
+00 TLV_ADDR_3 TLV_ADDR_2 TLV_ADDR_1 TLV_ADDR_0 TLV_LEN_H TLV_LEN_L ADDR_WIDTH MAX_PACKET_H MAX_PACKET_L CRC_H CRC_L
 ```
 
 响应 DATA 字段:
 
 | 字段 | 长度 | 说明 |
 |---|---:|---|
-| `CODE_ADDR` | 4 | CodeInfo TLV 信息块在 CODE 区内的字节地址 |
-| `CODE_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
-| `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址长度,只允许 `16` 或 `32` |
-| `ENDIAN_MARK` | 2 | 目标内存变量字节序标记;原始字节 `55 AA` 表示大端,原始字节 `AA 55` 表示小端 |
+| `TLV_ADDR` | 4 | CodeInfo TLV 信息块起始字节地址;`ADDR_WIDTH=16` 时读取帧只携带低 16 位 |
+| `TLV_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
+| `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址位宽,只允许 `16` 或 `32` |
 | `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度,包含 CMD/ADDR/LEN/DATA/CRC;为 0 表示未声明 |
 
-`ENDIAN_MARK` 只影响目标内存变量的多字节值显示和编辑,例如结构体字段、单独变量、enum 底层整数。`CMD`、`ADDR`、普通读写 `LEN`、`CODE_LEN`、`MAX_PACKET` 等多字节协议控制字段始终按大端序解析;TLV `LEN` 为单字节字段,不涉及大小端。
+描述符区域 `CODEINFO` 只读,不支持写入。`CMD`、`ADDR`、普通读写 `LEN`、`TLV_LEN`、`MAX_PACKET` 等多字节协议控制字段始终按大端序解析;TLV `LEN` 为单字节字段,不涉及大小端。
 
 ### 8.2 读取完整 CodeInfo
 
@@ -217,13 +209,11 @@ CodeInfo 同步分两步:
 ```text
 ADDR_WIDTH = 32: MODE = 0x07 ADDR32
 ADDR_WIDTH = 16: MODE = 0x04 CODE
-ADDR = CODE_ADDR
-LEN  = CODE_LEN
+ADDR = TLV_ADDR
+LEN  = TLV_LEN
 ```
 
-当 `ADDR_WIDTH=16` 时,下发普通读帧只携带 `CODE_ADDR` 的低 16 位。
-
-如果 `CODE_LEN` 超过当前 BLE 包长可承载的数据长度,小程序按协议自动分片读取。后续分片使用 bootstrap 返回的 `ADDR_WIDTH` 与 `MAX_PACKET`;当 `MAX_PACKET` 非 0 时,设置页最大包长和设备声明包长取较小值。
+如果 `TLV_LEN` 超过当前 BLE 包长可承载的数据长度,小程序按协议自动分片读取。
 
 最大数据长度计算:
 
@@ -234,9 +224,9 @@ ADDR_WIDTH=16: 读响应单帧数据上限 = max_frame_bytes - 7
 
 ## 9. CodeInfo TLV 信息块
 
-CodeInfo TLV 信息块位于 bootstrap 描述符给出的 CODE 区地址。该信息块不再定义固定 C 结构体头,也不再包含 `format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`。当前测试阶段只维护一种 TLV 字节格式,字段是否存在由 TLV `TYPE` 决定,信息块总长度由 `OP=0x05` bootstrap 返回的 `CODE_LEN` 决定。
+CodeInfo TLV 信息块位于 `CODEINFO` 描述符给出的地址。字段是否存在由 TLV `TYPE` 决定,信息块总长度由描述符返回的 `TLV_LEN` 决定。
 
-TLV 项连续排列,直到累计长度等于 `CODE_LEN`:
+TLV 项连续排列,直到累计长度等于 `TLV_LEN`:
 
 ```text
 TYPE LEN VALUE...
@@ -248,79 +238,77 @@ TYPE LEN VALUE...
 | `LEN` | 1 | `VALUE` 字节长度,单项最大 255 字节 |
 | `VALUE` | `LEN` | 类型对应的数据 |
 
-上位机解析到未知 `TYPE` 时跳过该项。任何 TLV 项声明长度超过 `CODE_LEN` 剩余字节时,整段 CodeInfo 视为格式错误。`CODE_LEN` 仍由 bootstrap 使用 16 位字段给出,因此整段 CodeInfo 可以超过 255 字节,只是单个 TLV 项不能超过 255 字节。
-
-### 9.1 板卡信息 TLV
-
-板卡信息全部为可选项。某块板不存在对应硬件信息时,不需要放该 TLV;小程序卡片和转换公式只使用实际同步到的字段。
-
-| TYPE | 名称 | LEN | VALUE 格式 | 显示/用途 |
-|---:|---|---:|---|---|
-| `0x01` | `cave_freq` | 1 | `uint8_t` | 载波频率,单位 KHz |
-| `0x02` | `ref_volt` | 1 | `uint8_t` | 基准电压实际值乘 10,显示时除以 10,单位 V |
-| `0x03` | `amp_gain` | 1 | `uint8_t` | 运放倍数,无单位 |
-| `0x04` | `rs_shunt` | 2 | `uint16_t` | 采样电阻,单位 mΩ |
-| `0x05` | `bus_div` | 4 | `float32` | 母线电压分压比,大端序 |
-| `0x06` | `along_div` | 4 | `float32` | 模拟输入电压分压比,大端序 |
-| `0x07` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号,建议 0 结尾或 0 填充 |
-| `0x08` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号,建议最多 30 字节,可容纳至少 7 个常见 UTF-8 汉字 |
+上位机解析到未知 `TYPE` 时跳过该项。任何 TLV 项声明长度超过 `TLV_LEN` 剩余字节时,整段 CodeInfo 视为格式错误。`TLV_LEN` 由描述符使用 16 位字段给出,因此整段 CodeInfo 可以超过 255 字节,只是单个 TLV 项不能超过 255 字节。
 
-### 9.2 内存入口 TLV
+### 9.1 固定内存入口 TLV
 
 结构体实例和单独变量也是 TLV 项,不再额外保存 `entry_kind` 字段:
 
-| TYPE | 类型 | 地址宽度 | 小程序处理 |
+| TYPE | 名称 | 地址宽度 | 小程序处理 |
 |---:|---|---:|---|
-| `0x20` | 结构体实例 | 16 位 | 创建结构体组;未导入定义时按 `00`、`01`、`02`... 字节占位 |
-| `0x21` | 单独变量 | 16 位 | 创建单变量组;1/2/4 字节默认按 `uint8_t`、`uint16_t`、`uint32_t` 显示,导入 enum 后可按枚举项显示 |
-| `0x28` | 结构体实例 | 32 位 | 创建结构体组,统一 32 位字节地址 |
-| `0x29` | 单独变量 | 32 位 | 创建单变量组,统一 32 位字节地址 |
-
-内存入口 `VALUE` 的地址宽度由 TLV `TYPE` 决定,不再由 bootstrap 的 `ADDR_WIDTH` 决定。bootstrap `ADDR_WIDTH` 只表示“读取 CodeInfo 信息块本体”时使用 16 位 CODE 地址还是 32 位统一地址。名称字段为 UTF-8 或 ASCII,建议 0 结尾或 0 填充。
+| `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` |
 
-`TYPE=0x20/0x21` 的 16 位地址入口:
+16 位地址入口 `VALUE`
 
 | 字段 | 长度 | 说明 |
 |---|---:|---|
-| `mem_type` | 1 | 所在存储区域,使用 AREA 编号 |
 | `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址 |
 | `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
-| `type_name` | `LEN - 5` | 结构体定义名、变量名或 enum 类型名 |
+| `name` | 32 | 固定 32 字节名称字段,UTF-8 或 ASCII,建议 0 结尾或 0 填充;结构体定义名、变量名或 enum 类型名 |
 
-`TYPE=0x28/0x29` 的 32 位地址入口:
+因此 16 位地址固定内存入口 TLV 的 `LEN` 固定为 `0x24`。
+
+32 位地址入口 `VALUE`:
 
 | 字段 | 长度 | 说明 |
 |---|---:|---|
 | `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址 |
 | `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
-| `type_name` | `LEN - 6` | 结构体定义名、变量名或 enum 类型名 |
+| `name` | 32 | 固定 32 字节名称字段,UTF-8 或 ASCII,建议 0 结尾或 0 填充;结构体定义名、变量名或 enum 类型名 |
+
+因此 32 位地址固定内存入口 TLV 的 `LEN` 固定为 `0x26`。
 
-32 位地址入口不携带 `mem_type`,小程序同步出的参数组统一使用 `MODE=0x07 ADDR32`。16 位地址入口按 `mem_type=0x01..0x04` 使用 DATA/IDATA/XDATA/CODE。手动存储访问卡片始终允许选择 `ADDR32/DATA/IDATA/XDATA/CODE`,不再因为 bootstrap `ADDR_WIDTH` 被锁定。
+### 9.2 自定义 TLV
 
-`mem_type`:
+`0x20~0x3F` 为自定义 TLV 区域。上位机保留原始 TLV 项展示,当前不对自定义项做业务解析。
 
-| 值 | 区域 |
-|---:|---|
-| `0x01` | DATA |
-| `0x02` | IDATA |
-| `0x03` | XDATA |
-| `0x04` | CODE |
+### 9.3 板卡信息 TLV
+
+板卡信息从 `0x40` 开始递增,全部为可选项。某块板不存在对应硬件信息时,不需要放该 TLV;小程序卡片和转换公式只使用实际同步到的字段。
+
+| TYPE | 名称 | LEN | VALUE 格式 | 显示/用途 |
+|---:|---|---:|---|---|
+| `0x40` | `cave_freq` | 1 | `uint8_t` | 载波频率,单位 KHz |
+| `0x41` | `ref_volt` | 1 | `uint8_t` | 基准电压实际值乘 10,显示时除以 10,单位 V |
+| `0x42` | `amp_gain` | 1 | `uint8_t` | 运放倍数,无单位 |
+| `0x43` | `rs_shunt` | 2 | `uint16_t` | 采样电阻,单位 mΩ |
+| `0x44` | `bus_div` | 4 | `float32` | 母线电压分压比,大端序 |
+| `0x45` | `along_div` | 4 | `float32` | 模拟输入电压分压比,大端序 |
+| `0x46` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号,建议 0 结尾或 0 填充 |
+| `0x47` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号,建议最多 30 字节,可容纳至少 7 个常见 UTF-8 汉字 |
 
 ## 10. 小程序同步后的处理
 
 小程序读取完整 CodeInfo TLV 信息块后:
 
 1. 按 `TYPE LEN VALUE` 遍历 TLV 项,未知类型跳过。
-2. 解析可选板卡信息,更新通讯页 CodeInfo 卡片;不存在的字段不显示、不进入转换公式上下文。
-3. 遇到 `TYPE=0x20/0x28` 创建结构体组;结构体定义未导入时按字节占位。
-4. 遇到 `TYPE=0x21/0x29` 创建单独变量组
-5. 按 TLV `TYPE` 解析内存入口地址;`0x20/0x21` 为 16 位地址,`0x28/0x29` 为 32 位地址
-6. 按 bootstrap `MEMORY_ENDIAN` 解释结构体字段、单独变量和 enum 底层整数的多字节值;单字节值不受影响。
-7. 根据 `mem_type`、`byte_addr`、`byte_len`、`TYPE`、`type_name` 创建参数组。
-8. 如果导入了 C 结构体和 enum 定义,只有结构体入口 `TYPE=0x20/0x28` 且 `type_name` 与结构体定义名一致、长度一致时才补全结构体字段;结构体字段类型为 enum 时,读回值按枚举项显示。
-9. 单独变量入口 `TYPE=0x21/0x29` 如果 `type_name` 匹配 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `type_name`,读回值按枚举项显示。
+2. 解析 `0x40` 起的可选板卡信息,更新通讯页 CodeInfo 卡片;不存在的字段不显示、不进入转换公式上下文。
+3. 遇到固定结构体入口 `TYPE=0x01/0x03/0x05/0x07` 创建结构体组;结构体定义未导入时按字节占位。
+4. 遇到固定变量入口 `TYPE=0x02/0x04/0x06/0x08` 创建单独变量组,初始按 `byte_len` 显示原始字节并标记为未配置
+5. 按固定 TLV `TYPE` 解析内存区域和地址宽度
+6. 当前目标内存多字节值默认按大端解释;单字节值不受影响。
+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. 如果存在相同 `type_name` 但 `mem_type` 或 `byte_addr` 不同的入口,应视为不同参数组。
+11. 如果存在相同 `name` 但区域或 `byte_addr` 不同的入口,应视为不同参数组。
 
 ## 11. 示例
 
@@ -334,18 +322,24 @@ LEN = 0x0040
 正常响应 = 03 20 00 00 40 DATA(64B) CRC_H CRC_L
 ```
 
-### 11.2 读取 CODE 中 CodeInfo
+### 11.2 读取 CodeInfo
 
 假设描述符返回:
 
 ```text
-CODE_ADDR = 0x00123456
-CODE_LEN  = 0x0120
+TLV_ADDR = 0x00123456
+TLV_LEN  = 0x0120
 ADDR_WIDTH = 32
-MEMORY_ENDIAN = big,bootstrap 原始标记 55 AA
+MAX_PACKET = 0x0040
 ```
 
-读取请求:
+先读取 `CODEINFO` 描述符:
+
+```text
+00 CRC_H CRC_L
+```
+
+再按 `ADDR_WIDTH=32` 读取 TLV 信息块:
 
 ```text
 07 00 12 34 56 01 20 CRC_H CRC_L
@@ -358,47 +352,42 @@ MEMORY_ENDIAN = big,bootstrap 原始标记 55 AA
 结构体实例:
 
 ```text
-TYPE = 0x20
-LEN = 0x14
-VALUE = 03 20 00 00 40 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
+TYPE = 0x05
+LEN = 0x24
+VALUE = 20 00 00 40 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 含义 = XDATA 0x2000 / 64 bytes / Motor_Runtime_t
 ```
 
 单独变量:
 
 ```text
-TYPE = 0x29
-LEN = 0x0F
-VALUE = 00 00 21 00 00 02 73 70 65 65 64 5F 72 65 66
+TYPE = 0x08
+LEN = 0x26
+VALUE = 00 00 21 00 00 02 73 70 65 65 64 5F 72 65 66 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 含义 = ADDR32 0x00002100 / 2 bytes / speed_ref
 ```
 
 ## 12. 实现约束
 
 1. 普通内存读写帧的 `LEN` 单位始终为字节,字段固定 16 位,有效范围为 `1..0xFFFF`;CodeInfo TLV 项内的 `LEN` 为 1 字节,只表示单项 `VALUE` 长度。
-2. `ADDR` 单位始终为字节;`MODE=0x07` 时地址字段为 32 位,`MODE=0x01..0x04` 时地址字段为 16 位;`MODE=0x00/0x05/0x06` 保留。
+2. `ADDR` 单位始终为字节;`MODE=0x07` 时地址字段为 32 位,`MODE=0x01..0x04` 时地址字段为 16 位;`MODE=0x00` 为 CODEINFO 短描述符请求;`MODE=0x05..0x06` 保留。
 3. 普通读写响应必须回显请求的 `CMD`、`ADDR`、`LEN`。
 4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC,并确认数据长度等于 `LEN`。
-5. `CODE` 区只读,写入必须返回 `WRITE_PROTECT`。
-6. 特殊指令正常响应必须包含 `STATUS`
-7. CodeInfo 同步必须走 `OP=0x05` bootstrap 描述符,再按 bootstrap `ADDR_WIDTH` 读取信息块;内存入口地址宽度由 TLV `TYPE` 决定,目标变量值按 bootstrap `MEMORY_ENDIAN` 解释
+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` 决定
 8. 固件更新 CodeInfo TLV 类型或内存入口格式时,必须同步更新本文档和小程序解析器。
 
 ## 13. 读写位宽说明
 
 普通内存读写协议不额外携带变量位宽字段,只传输“字节地址 + 字节长度 + 字节数据”。变量显示和编辑位宽由以下信息决定:
 
-- `TYPE=0x20/0x28` 的结构体实例由导入的 C 结构体定义决定字段类型。
-- `TYPE=0x21/0x29` 的单独变量由 `byte_len` 默认推断:1 字节为 `uint8_t`,2 字节为 `uint16_t`,4 字节为 `uint32_t`
-- `enum` 不改变普通内存读写帧,只是在导入定义后给整数值增加枚举映射;显示格式为 `枚举项 (数值)`,写入时可输入枚举项名称或数字。
+- `TYPE=0x01/0x03/0x05/0x07` 的结构体实例由导入的 C 结构体定义决定字段类型。
+- `TYPE=0x02/0x04/0x06/0x08` 的单独变量由 `byte_len` 给出字节长度,UI 必须选择与该长度一致的显示/编辑类型;未配置前只显示原始字节,不允许写入
+- `enum` 不改变普通内存读写帧,只是在导入定义后给同长度整数值增加枚举映射;显示格式为 `枚举项 (数值)`,写入时可输入枚举项名称或数字。
 - 参数页可在寄存器/变量配置中进一步设置公式、单位、范围和显示类型。
 
-如果后续从机需要原子读写、硬件寄存器位宽或对齐语义,可以新增 typed 特殊指令,不改变普通内存访问帧:
-
-```text
-读: 4F 06 AREA ADDR_3 ADDR_2 ADDR_1 ADDR_0 VALUE_WIDTH COUNT_H COUNT_L CRC_H CRC_L
-写: 4F 07 AREA ADDR_3 ADDR_2 ADDR_1 ADDR_0 VALUE_WIDTH COUNT_H COUNT_L DATA... CRC_H CRC_L
-```
+如果后续从机需要原子读写、硬件寄存器位宽或对齐语义,可以新增 `bit6=1` 的 typed 特殊指令,不改变普通内存访问帧。具体 `special_code` 和 DATA 格式需另行定义。
 
 建议字段:
 

+ 36 - 42
完整协议说明.md

@@ -92,18 +92,18 @@ CRC 使用 `CRC16-CCITT-FALSE`,高字节在前。所有多字节协议字段
 `CMD` 位定义:
 
 ```text
-bit7      ERR       异常标志
-bit6      CTL       特殊指令标志位
+bit7      ERR       故障标志,仅回帧出现
+bit6      SPECIAL   特殊指令标志位
 bit5~bit4 RSV       保留,普通读写保持 0
-bit3      RW        读写标志,0=读,1=写
-bit2~bit0 MODE      地址模式或存储区域
+bit3      RW        普通读写标志,0=读,1=写
+bit2~bit0 MODE      普通读写区域码
 ```
 
 地址模式与存储区域:
 
 | MODE | 名称 | 地址宽度 | 读 | 写 | 说明 |
 |---:|---|---|---|---|---|
-| `0x00` | 保留 | - | 禁止 | 禁止 | 保留 |
+| `0x00` | CODEINFO | 16 位 | 支持 | 禁止 | 同步时读取一次,返回 TLV 起始地址与长度 |
 | `0x01` | DATA | 16 位 | 支持 | 支持 | 内部直接寻址 RAM |
 | `0x02` | IDATA | 16 位 | 支持 | 支持 | 内部间接寻址 RAM |
 | `0x03` | XDATA | 16 位 | 支持 | 支持 | 外部数据空间或扩展 RAM |
@@ -129,25 +129,21 @@ CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
 CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 ```
 
-普通写响应只回显 `CMD`、`ADDR`、`LEN`。写 CODE 区必须返回写保护异常。
+普通写响应只回显 `CMD`、`ADDR`、`LEN`。写 CODEINFO 或 CODE 区必须返回写保护异常。
 
-特殊指令使用固定 `CMD=0x4F`:
+特殊指令使用 `CMD bit6=1`:
 
 ```text
-请求: 4F OP DATA... CRC_H CRC_L
-响应: 4F OP STATUS DATA... CRC_H CRC_L
-异常: CF EXCEPTION_CODE CRC_H CRC_L
+请求: CMD DATA... CRC_H CRC_L
+响应: CMD DATA... CRC_H CRC_L
+异常: CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 ```
 
 特殊指令表:
 
-| OP | 名称 | 请求 DATA | 成功响应 DATA | 用途 |
-|---:|---|---|---|---|
-| `0x01` | RESET | 无 | 无 | 复位 |
-| `0x02` | START | 无 | 无 | 启动 |
-| `0x03` | STOP | 无 | 无 | 停止 |
-| `0x04` | SET_CONTROL_REF | `REF_H REF_L` | 无 | 控制参考值,`int16_t` 大端 |
-| `0x05` | READ_CODE_INFO_DESCRIPTOR | 无 | `CODE_ADDR32 + CODE_LEN16 + ADDR_WIDTH8 + MEMORY_ENDIAN16 + MAX_PACKET16` | 读取 CodeInfo bootstrap 描述符 |
+| 指令码 | CMD | 名称 | 请求 DATA | 成功响应 DATA | 用途 |
+|---:|---:|---|---|---|---|
+| `0x01` | `0x41` | RESET | 无 | 无 | 复位 |
 
 完整帧细节、异常码、示例和从机参考见 `存储访问协议.md`。
 
@@ -155,10 +151,10 @@ CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 
 存储访问同步固定分两步:
 
-1. 发送特殊指令 `4F 05 CRC_H CRC_L`,读取 CodeInfo bootstrap 描述符
-2. 根据 bootstrap 返回的 `CODE_ADDR32 + CODE_LEN16 + ADDR_WIDTH8 + MEMORY_ENDIAN16 + MAX_PACKET16` 读取完整 CodeInfo TLV 信息块。`ADDR_WIDTH=32` 使用 `MODE=0x07 ADDR32`,`ADDR_WIDTH=16` 使用 `MODE=0x04 CODE`;`MAX_PACKET` 非 0 时与设置页最大包长取较小值
+1. 发送 `00 CRC_H CRC_L` 读取 `area=0x00 CODEINFO` 描述符,数据区为 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16`
+2. 根据描述符返回的地址、长度、地址位宽和最大包长读取完整 CodeInfo TLV 信息块;`ADDR_WIDTH=16` 使用 `CODE` 区 16 位地址,`ADDR_WIDTH=32` 使用 `ADDR32`
 
-bootstrap 中 `MEMORY_ENDIAN16` 使用原始字节标记目标内存变量字节序:`55 AA` 表示变量值大端,`AA 55` 表示变量值小端。多字节协议控制字段始终固定大端,包括 `CMD`、`ADDR`、普通读写 `LEN`、`CODE_LEN` 和 `MAX_PACKET`;TLV `LEN` 为单字节字段,不涉及大小端。
+多字节协议控制字段始终固定大端,包括 `CMD`、`ADDR`、普通读写 `LEN`、描述符 `TLV_LEN` 和 `MAX_PACKET`;TLV `LEN` 为单字节字段,不涉及大小端。当前目标内存变量的多字节值默认按大端解码和写入。
 
 CodeInfo 信息块使用纯 TLV 格式,不再包含固定头、`format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`:
 
@@ -170,33 +166,31 @@ TYPE LEN VALUE...
 
 | TYPE | 名称 | VALUE |
 |---:|---|---|
-| `0x01` | `cave_freq` | `uint8_t`,KHz |
-| `0x02` | `ref_volt` | `uint8_t`,实际参考电压乘 10 |
-| `0x03` | `amp_gain` | `uint8_t` |
-| `0x04` | `rs_shunt` | `uint16_t`,mΩ |
-| `0x05` | `bus_div` | `float32`,大端 |
-| `0x06` | `along_div` | `float32`,大端 |
-| `0x07` | `chip_model` | UTF-8 或 ASCII 字符串 |
-| `0x08` | `model` | UTF-8 或 ASCII 字符串 |
-| `0x20` | 16 位地址结构体实例 | `mem_type + byte_addr16 + byte_len16 + type_name` |
-| `0x21` | 16 位地址单独变量 | `mem_type + byte_addr16 + byte_len16 + type_name` |
-| `0x28` | 32 位地址结构体实例 | `byte_addr32 + byte_len16 + type_name` |
-| `0x29` | 32 位地址单独变量 | `byte_addr32 + byte_len16 + type_name` |
-
-TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 bootstrap `CODE_LEN16` 决定。内存入口 TLV 的地址宽度由 `TYPE` 决定,bootstrap `ADDR_WIDTH` 只决定读取 CodeInfo 信息块本体时使用 16 位 CODE 地址还是 32 位统一地址。`0x20/0x21` 按 `mem_type=0x01..0x04` 使用 DATA/IDATA/XDATA/CODE,`0x28/0x29` 统一使用 `MODE=0x07 ADDR32` 且不携带 `mem_type`。
+| `0x01` | DATA 结构体实例 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x02` | DATA 单独变量 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x03` | IDATA 结构体实例 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x04` | IDATA 单独变量 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x05` | XDATA 结构体实例 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x06` | XDATA 单独变量 | `byte_addr16 + byte_len16 + name[32]`,`LEN=0x24` |
+| `0x07` | ADDR32 结构体实例 | `byte_addr32 + byte_len16 + name[32]`,`LEN=0x26` |
+| `0x08` | ADDR32 单独变量 | `byte_addr32 + byte_len16 + name[32]`,`LEN=0x26` |
+| `0x20..0x3F` | 自定义 TLV | 上位机保留原始项,当前跳过业务解析 |
+| `0x40..` | 板卡参数 | `0x40` 起按 `cave_freq/ref_volt/...` 递增 |
+
+TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 CODEINFO 描述符 `TLV_LEN16` 决定。内存入口 TLV 的地址宽度和区域由 `TYPE` 决定,名称字段固定 32 字节。
 
 同步到参数页后的规则:
 
 | 条件 | 处理 |
 |---|---|
-| `TYPE=0x20/0x28` 且未导入结构体定义 | 创建结构体组,每个字节按 `00`、`01`、`02` 命名 |
-| `TYPE=0x20/0x28` 且导入定义名、长度匹配 | 按 C 结构体字段展示,保留多字节字段整体值;字段类型为 enum 时按枚举项显示 |
-| `TYPE=0x21/0x29` | 创建单变量组,1/2/4 字节默认推断为 `uint8_t`、`uint16_t`、`uint32_t`;匹配 enum 后按枚举项显示 |
+| `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 后按枚举项显示 |
 | 相同名称但地址或区域不同 | 视为不同参数组 |
 | 当前已有同区域、同地址、同名称、同长度且已导入定义 | 保留当前导入结构,只更新同步来源信息 |
 | 未知 TLV `TYPE` | 跳过 |
 
-结构体和 enum 可在同一个 `.h/.c/.txt` 定义文件中导入。单独变量的 enum 匹配支持两种方式:`type_name` 直接等于 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名等于 `type_name`。enum 只影响显示与输入映射,不改变底层字节读写协议。
+结构体和 enum 可在同一个 `.h/.c/.txt` 定义文件中导入。单独变量的 enum 匹配支持两种方式:入口 `name` 直接等于 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名等于入口 `name`。enum 只影响显示与输入映射,不改变底层字节读写协议。
 
 CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信息 TLV 不存在时对应字段不显示;电机型号优先按 UTF-8 解析,也兼容纯 ASCII。
 
@@ -204,7 +198,7 @@ CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信
 
 参数页读回的数据先按字段类型解码为原始值,再根据可选转换公式显示实际值。公式由参数配置自定义,适合标幺值转实际值、比例缩放和偏置换算。
 
-存储访问参数组的多字节变量值按 bootstrap `MEMORY_ENDIAN` 解码和写入。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
+存储访问参数组的多字节变量值当前默认按大端解码和写入。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
 
 公式支持变量:
 
@@ -217,9 +211,9 @@ CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信
 | `rsShunt` | CodeInfo 采样电阻 |
 | `busDiv` | CodeInfo 母线电压分压比 |
 | `alongDiv` | CodeInfo 模拟输入电压分压比 |
-| `maxPacketLength` | 同步描述符声明的最大完整协议帧长度 |
-| `addressWidth` | 同步描述符声明的地址长度 |
-| `memoryEndian` | 同步 bootstrap 声明的目标内存变量字节序,`big` 或 `little` |
+| `maxPacketLength` | 当前同步上下文的最大完整协议帧长度,未声明时为 0 |
+| `addressWidth` | CodeInfo 本体读取地址长度,当前固定为 32 |
+| `memoryEndian` | 当前目标内存变量字节序,默认 `big` |
 
 公式只支持数字、括号和 `+ - * /`,不执行任意脚本。