Bläddra i källkod

添加私有协议,移除电机协议解析

avery 13 timmar sedan
förälder
incheckning
6957a2b990
49 ändrade filer med 2702 tillägg och 5469 borttagningar
  1. 0 7
      app.json
  2. 106 0
      app.wxss
  3. 190 0
      domain/generic-modbus/code-info-parser.js
  4. 1 0
      domain/generic-modbus/index.js
  5. 361 28
      domain/generic-modbus/model.js
  6. 257 5
      domain/generic-modbus/struct-parser.js
  7. 0 127
      domain/motor-control/calculation-context.js
  8. 0 483
      domain/motor-control/control-state.js
  9. 0 281
      domain/motor-control/conversions.js
  10. 0 21
      domain/motor-control/data.js
  11. 0 7
      domain/motor-control/index.js
  12. 0 29
      domain/motor-control/input-value-utils.js
  13. 0 890
      domain/motor-control/params-state.js
  14. 0 104
      domain/motor-control/register-groups.js
  15. 0 445
      domain/motor-control/registers.js
  16. 0 109
      domain/motor-control/status-format.js
  17. 0 120
      domain/motor-control/status-state.js
  18. 0 130
      domain/motor-control/thermistor.js
  19. 1 15
      features/bootloader/service.js
  20. 34 1
      features/generic-modbus/poller.js
  21. 878 27
      features/generic-modbus/service.js
  22. 0 35
      features/home/service.js
  23. 0 5
      features/home/view-model.js
  24. 0 459
      features/motor-control/control-service.js
  25. 0 21
      features/motor-control/control-view-model.js
  26. 0 16
      features/motor-control/index.js
  27. 0 234
      features/motor-control/params-service.js
  28. 0 58
      features/motor-control/protocol-service.js
  29. 0 182
      features/motor-control/sync-service.js
  30. 35 117
      features/private-protocol/params-view-model.js
  31. 17 20
      features/settings/view-model.js
  32. 1 0
      features/tools/navigation.js
  33. 0 6
      pages/home/home.js
  34. 15 11
      pages/home/home.wxml
  35. 0 121
      pages/index/index.js
  36. 0 5
      pages/index/index.json
  37. 0 167
      pages/index/index.wxml
  38. 0 316
      pages/index/index.wxss
  39. 72 280
      pages/params/params.js
  40. 20 482
      pages/params/params.wxml
  41. 60 9
      pages/settings/settings.js
  42. 76 37
      pages/settings/settings.wxml
  43. 149 0
      pages/settings/settings.wxss
  44. 191 3
      protocols/modbus-rtu/client.js
  45. 101 0
      protocols/modbus-rtu/frame.js
  46. 116 1
      protocols/modbus-rtu/response.js
  47. 6 1
      protocols/transport-helpers.js
  48. 15 46
      store/settings-store.js
  49. 0 8
      store/theme-store.js

+ 0 - 7
app.json

@@ -2,7 +2,6 @@
   "darkmode": true,
   "pages": [
     "pages/home/home",
-    "pages/index/index",
     "pages/params/params",
     "pages/settings/settings"
   ],
@@ -22,12 +21,6 @@
         "iconPath": "assets/tab/home.png",
         "selectedIconPath": "assets/tab/home-active.png"
       },
-      {
-        "pagePath": "pages/index/index",
-        "text": "控制",
-        "iconPath": "assets/tab/control.png",
-        "selectedIconPath": "assets/tab/control-active.png"
-      },
       {
         "pagePath": "pages/params/params",
         "text": "参数",

+ 106 - 0
app.wxss

@@ -380,6 +380,13 @@ page {
   margin-left: 0;
 }
 
+.generic-protocol-actions {
+  flex: 1;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 8rpx;
+}
+
 .param-entry-panel .panel-title {
   flex: 1;
   white-space: nowrap;
@@ -713,6 +720,21 @@ page {
   top: 7rpx;
   width: 24rpx;
   height: 24rpx;
+  z-index: 1;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: contain;
+  opacity: 0.96;
+}
+
+.panel-icon-image {
+  position: absolute;
+  z-index: 2;
+  left: 7rpx;
+  top: 7rpx;
+  width: 24rpx;
+  height: 24rpx;
+  pointer-events: none;
 }
 
 .panel-icon::after {
@@ -729,96 +751,172 @@ page {
   --icon-end: #2f5e7c;
 }
 
+.icon-chip::before {
+  background-image: url("/assets/icons/chip-white.png");
+}
+
 .icon-control {
   --icon-start: #18a58b;
   --icon-end: #0e746f;
 }
 
+.icon-control::before {
+  background-image: url("/assets/icons/control-white.png");
+}
+
 .icon-bluetooth {
   --icon-start: #16a8cf;
   --icon-end: #0f7a9a;
 }
 
+.icon-bluetooth::before {
+  background-image: url("/assets/icons/bluetooth-connected-white.png");
+}
+
 .icon-radar {
   --icon-start: #23b0d7;
   --icon-end: #137e8f;
 }
 
+.icon-radar::before {
+  background-image: url("/assets/icons/radar-white.png");
+}
+
 .icon-terminal {
   --icon-start: #63758f;
   --icon-end: #324056;
 }
 
+.icon-terminal::before {
+  background-image: url("/assets/icons/terminal-white.png");
+}
+
 .icon-send {
   --icon-start: #39bdf0;
   --icon-end: #1684c5;
 }
 
+.icon-send::before {
+  background-image: url("/assets/icons/send-white.png");
+}
+
 .icon-history {
   --icon-start: #64748b;
   --icon-end: #475569;
 }
 
+.icon-history::before {
+  background-image: url("/assets/icons/history-white.png");
+}
+
 .icon-status {
   --icon-start: #14a79a;
   --icon-end: #2563eb;
 }
 
+.icon-status::before {
+  background-image: url("/assets/icons/status-white.png");
+}
+
 .icon-bars {
   --icon-start: #148f85;
   --icon-end: #105f8b;
 }
 
+.icon-bars::before {
+  background-image: url("/assets/icons/sliders-white.png");
+}
+
 .icon-tune {
   --icon-start: #17a59f;
   --icon-end: #0d7280;
 }
 
+.icon-tune::before {
+  background-image: url("/assets/icons/sliders-white.png");
+}
+
 .icon-speed {
   --icon-start: #f7a623;
   --icon-end: #d97f0c;
 }
 
+.icon-speed::before {
+  background-image: url("/assets/icons/speed-white.png");
+}
+
 .icon-target {
   --icon-start: #21a37e;
   --icon-end: #0f766e;
 }
 
+.icon-target::before {
+  background-image: url("/assets/icons/target-white.png");
+}
+
 .icon-shield-check {
   --icon-start: #16a34a;
   --icon-end: #0f766e;
 }
 
+.icon-shield-check::before {
+  background-image: url("/assets/icons/shield-check-white.png");
+}
+
 .icon-crc {
   --icon-start: #2563eb;
   --icon-end: #0f766e;
 }
 
+.icon-crc::before {
+  background-image: url("/assets/icons/hash-white.png");
+}
+
 .icon-filter {
   --icon-start: #f59e0b;
   --icon-end: #d97706;
 }
 
+.icon-filter::before {
+  background-image: url("/assets/icons/funnel-white.png");
+}
+
 .icon-reactance {
   --icon-start: #0ea5e9;
   --icon-end: #0f766e;
 }
 
+.icon-reactance::before {
+  background-image: url("/assets/icons/audio-waveform-white.png");
+}
+
 .icon-smd {
   --icon-start: #64748b;
   --icon-end: #334155;
 }
 
+.icon-smd::before {
+  background-image: url("/assets/icons/microchip-white.png");
+}
+
 .icon-snow {
   --icon-start: #38bdf8;
   --icon-end: #2563eb;
 }
 
+.icon-snow::before {
+  background-image: url("/assets/icons/snowflake-white.png");
+}
+
 .icon-three-phase {
   --icon-start: #7c3aed;
   --icon-end: #2563eb;
 }
 
+.icon-three-phase::before {
+  background-image: url("/assets/icons/zap-white.png");
+}
+
 .param-row {
   display: flex;
   align-items: center;
@@ -2102,6 +2200,14 @@ page {
   width: 86rpx;
 }
 
+.subpage-fixed-header--generic .generic-protocol-actions {
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 8rpx;
+  margin-left: 0;
+}
+
 .auto-read-button {
   margin-left: 2rpx;
 }

+ 190 - 0
domain/generic-modbus/code-info-parser.js

@@ -0,0 +1,190 @@
+const {
+  bytesToHex,
+  trimTrailingNullBytes
+} = require('../../utils/binary-utils.js')
+
+const FIXED_HEADER_BYTE_LENGTH = 44
+const STRUCT_ENTRY_MIN_BYTE_LENGTH = 5
+const MEMORY_TYPE_AREAS = {
+  0x00: 'DATA',
+  0x01: 'IDATA',
+  0x02: 'XDATA',
+  0x03: 'CODE'
+}
+
+function toBytes(bytes) {
+  return Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
+}
+
+function readUint16(bytes, offset) {
+  if (offset + 1 >= bytes.length) return 0
+
+  return (((bytes[offset] || 0) << 8) | (bytes[offset + 1] || 0)) & 0xFFFF
+}
+
+function readFloat(bytes, offset) {
+  if (offset + 3 >= bytes.length) return 0
+
+  const buffer = new ArrayBuffer(4)
+  const view = new DataView(buffer)
+  for (let index = 0; index < 4; index += 1) {
+    view.setUint8(index, bytes[offset + index] || 0)
+  }
+
+  return view.getFloat32(0, false)
+}
+
+function readAscii(bytes, offset, byteLength) {
+  const source = trimTrailingNullBytes(bytes.slice(offset, offset + byteLength))
+
+  return source
+    .map((byte) => (byte >= 0x20 && byte <= 0x7E ? String.fromCharCode(byte) : ''))
+    .join('')
+    .trim()
+}
+
+function formatAddress(address) {
+  return `0x${Number(address || 0).toString(16).toUpperCase().padStart(4, '0')}`
+}
+
+function normalizeTypeName(value, fallback) {
+  const text = String(value || '').trim()
+
+  return text || fallback
+}
+
+function parseStructEntry(bytes, offset, entryLength, index) {
+  const byteAddr = readUint16(bytes, offset)
+  const byteLength = readUint16(bytes, offset + 2)
+  const memType = bytes[offset + 4] & 0xFF
+  const nameByteLength = Math.max(0, entryLength - STRUCT_ENTRY_MIN_BYTE_LENGTH)
+  const typeName = normalizeTypeName(
+    readAscii(bytes, offset + STRUCT_ENTRY_MIN_BYTE_LENGTH, nameByteLength),
+    `struct_${index + 1}`
+  )
+  const memoryArea = MEMORY_TYPE_AREAS[memType] || 'UNKNOWN'
+
+  return {
+    byteAddr,
+    byteLength,
+    index,
+    memType,
+    memoryArea,
+    sourceAddressText: formatAddress(byteAddr),
+    typeName
+  }
+}
+
+function parseModbusCodeInfo(bytes) {
+  const source = toBytes(bytes)
+  if (source.length < FIXED_HEADER_BYTE_LENGTH) {
+    throw new Error('Code信息块长度不足,无法解析 Modbus_Code_Info_t 固定头')
+  }
+
+  const byteLength = readUint16(source, 0)
+  const structCount = readUint16(source, 40)
+  const structEntryLength = readUint16(source, 42)
+  if (structEntryLength < STRUCT_ENTRY_MIN_BYTE_LENGTH) {
+    throw new Error('Code信息块 struct_entry_len 无效')
+  }
+
+  const availableTableBytes = Math.max(0, source.length - FIXED_HEADER_BYTE_LENGTH)
+  const tableCapacity = Math.floor(availableTableBytes / structEntryLength)
+  const safeStructCount = Math.min(structCount, tableCapacity)
+  const structTable = []
+
+  for (let index = 0; index < safeStructCount; index += 1) {
+    structTable.push(parseStructEntry(
+      source,
+      FIXED_HEADER_BYTE_LENGTH + index * structEntryLength,
+      structEntryLength,
+      index
+    ))
+  }
+
+  return {
+    alongDiv: readFloat(source, 12),
+    ampGain: readUint16(source, 4),
+    busDiv: readFloat(source, 8),
+    byteLength,
+    caveFreq: source[2] & 0xFF,
+    chipModel: readAscii(source, 16, 8),
+    model: readAscii(source, 24, 16),
+    rawHex: bytesToHex(source, ' '),
+    refVolt: source[3] & 0xFF,
+    rsShunt: readUint16(source, 6),
+    structCount,
+    structEntryLength,
+    structTable,
+    tableCapacity,
+    truncated: safeStructCount < structCount
+  }
+}
+
+function createRegistersForByteSpan(entry) {
+  const count = Math.max(1, Number(entry.byteLength) || 1)
+  const registers = []
+
+  for (let offset = 0; offset < count; offset += 1) {
+    registers.push({
+      byteStart: offset,
+      dataType: 'uint8_t',
+      name: `${entry.typeName}[${offset.toString(16).toUpperCase().padStart(2, '0')}]`,
+      remark: `${entry.memoryArea} ${formatAddress(entry.byteAddr + offset)} · ${entry.typeName}`,
+      sourceAddress: (entry.byteAddr + offset) & 0xFFFF,
+      sourceAddressText: formatAddress((entry.byteAddr + offset) & 0xFFFF),
+      sourceByteLength: 1,
+      sourceMemoryArea: entry.memoryArea,
+      sourceMemoryClass: entry.memoryArea,
+      sourceSymbolName: entry.typeName,
+      sourceSymbolType: entry.typeName
+    })
+  }
+
+  return registers
+}
+
+function createGroupsFromCodeInfo(codeInfo, options = {}) {
+  const maxRegisters = Math.max(1, Number(options.maxRegistersPerGroup) || 256)
+  const groups = []
+
+  codeInfo.structTable.forEach((entry) => {
+    if (!entry.byteLength || entry.memoryArea === 'UNKNOWN') return
+
+    for (let offset = 0; offset < entry.byteLength; offset += maxRegisters) {
+      const chunkLength = Math.min(maxRegisters, entry.byteLength - offset)
+      const chunkEntry = {
+        ...entry,
+        byteAddr: (entry.byteAddr + offset) & 0xFFFF,
+        byteLength: chunkLength
+      }
+      const suffix = entry.byteLength > maxRegisters ? ` #${Math.floor(offset / maxRegisters) + 1}` : ''
+
+      groups.push({
+        addressUnit: 'byte',
+        layout: 'struct',
+        name: `${entry.memoryArea} ${entry.typeName}${suffix}`,
+        quantity: chunkLength,
+        registerType: entry.memoryArea === 'CODE' ? 'input' : 'holding',
+        registers: createRegistersForByteSpan(chunkEntry),
+        sourceAddress: chunkEntry.byteAddr,
+        sourceAddressText: formatAddress(chunkEntry.byteAddr),
+        sourceByteLength: chunkLength,
+        sourceMemoryArea: entry.memoryArea,
+        sourceMemoryClass: entry.memoryArea,
+        sourceSegment: 'Modbus_Code_Info_t',
+        sourceSegmentModule: '',
+        sourceSymbolName: entry.typeName,
+        startAddress: formatAddress(chunkEntry.byteAddr)
+      })
+    }
+  })
+
+  return groups
+}
+
+module.exports = {
+  FIXED_HEADER_BYTE_LENGTH,
+  createGroupsFromCodeInfo,
+  parseModbusCodeInfo
+}

+ 1 - 0
domain/generic-modbus/index.js

@@ -1,4 +1,5 @@
 module.exports = {
+  codeInfoParser: require('./code-info-parser.js'),
   model: require('./model.js'),
   structParser: require('./struct-parser.js')
 }

+ 361 - 28
domain/generic-modbus/model.js

@@ -122,6 +122,36 @@ const DEFAULT_REGISTER_TYPE = REGISTER_TYPE_OPTIONS[0].key
 const DEFAULT_DATA_TYPE = 'uint16_t'
 const GROUP_LAYOUT_REGISTER = 'register'
 const GROUP_LAYOUT_STRUCT = 'struct'
+const BYTE_ADDRESS_MEMORY_AREAS = ['BIT', 'CODE', 'DATA', 'IDATA', 'XDATA']
+const SOURCE_REGISTER_FIELDS = [
+  'sourceAddress',
+  'sourceAddressText',
+  'sourceByteLength',
+  'sourceBitOffset',
+  'sourceBitWidth',
+  'sourceMemoryArea',
+  'sourceMemoryClass',
+  'sourceSymbolName',
+  'sourceSymbolType'
+]
+const STRUCT_REGISTER_FIELDS = [
+  'bitOffset',
+  'bitWidth',
+  'byteStart',
+  'isBitField',
+  'structByteLength'
+]
+const SOURCE_GROUP_FIELDS = [
+  'addressUnit',
+  'sourceAddress',
+  'sourceAddressText',
+  'sourceByteLength',
+  'sourceMemoryArea',
+  'sourceMemoryClass',
+  'sourceSegment',
+  'sourceSegmentModule',
+  'sourceSymbolName'
+]
 
 function normalizeAddress(value, fallback = 0) {
   if (typeof value === 'number') {
@@ -206,7 +236,38 @@ function isStructLayout(layout) {
   return layout === GROUP_LAYOUT_STRUCT
 }
 
+function isBitFieldRegister(register = {}) {
+  return !!register.isBitField
+}
+
+function normalizeBitOffset(value) {
+  const numberValue = Math.floor(Number(value) || 0)
+
+  return Math.min(Math.max(numberValue, 0), 7)
+}
+
+function normalizeBitWidth(value) {
+  const numberValue = Math.round(Number(value) || 1)
+
+  return Math.min(Math.max(numberValue, 1), 32)
+}
+
+function getBitFieldByteLength(register = {}) {
+  const bitOffset = normalizeBitOffset(register.bitOffset)
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+
+  return Math.max(1, Math.ceil((bitOffset + bitWidth) / 8))
+}
+
+function getBitFieldMaxValue(register = {}) {
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+
+  return bitWidth >= 32 ? 0xFFFFFFFF : Math.pow(2, bitWidth) - 1
+}
+
 function getRegisterByteLength(dataType, register = {}) {
+  if (isBitFieldRegister(register)) return getBitFieldByteLength(register)
+
   const type = getDataType(dataType)
   if (type.kind === 'text') {
     const byteLength = getRegisterTextByteLength(register)
@@ -221,9 +282,13 @@ function getRegisterWordCount(dataType, register = {}) {
   return Math.max(1, Math.ceil(getRegisterByteLength(dataType, register) / 2))
 }
 
+function getByteSpanWordCount(byteOffset, byteLength) {
+  return Math.max(1, Math.ceil((Math.max(0, Number(byteOffset) || 0) + Math.max(1, Number(byteLength) || 1)) / 2))
+}
+
 function getRegisterWordCountAtOffset(dataType, byteOffset, register = {}) {
   const byteLength = getRegisterByteLength(dataType, register)
-  return Math.max(1, Math.ceil((byteOffset + byteLength) / 2))
+  return getByteSpanWordCount(byteOffset, byteLength)
 }
 
 function getEncodeByteLimit(register) {
@@ -270,6 +335,12 @@ function formatRawWordText(words = []) {
   return words.map((word) => `0x${padWordHex(word)}`).join(' ')
 }
 
+function formatRawByteText(bytes = []) {
+  if (!Array.isArray(bytes) || !bytes.length) return '--'
+
+  return bytes.map((byte) => `0x${(Number(byte) & 0xFF).toString(16).toUpperCase().padStart(2, '0')}`).join(' ')
+}
+
 function formatAddressRange(startAddress, wordCount) {
   const address = normalizeAddress(startAddress, 0)
   const count = Math.max(1, Number(wordCount) || 1)
@@ -282,6 +353,14 @@ function formatAddressRange(startAddress, wordCount) {
   return `0x${padWordHex(address)}-0x${padWordHex(safeEndAddress)}${overflowText}`
 }
 
+function isByteAddressedGroup(group = {}) {
+  const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()
+
+  return group.addressUnit === 'byte'
+    || group.addressUnit === 'bytes'
+    || BYTE_ADDRESS_MEMORY_AREAS.indexOf(memoryArea) >= 0
+}
+
 function formatRegisterAddressText(address, byteOffset, byteLength, registerType) {
   if (isBitRegisterType(registerType)) return `0x${padHex(address)}`
   if (byteLength === 1) return `0x${padHex(address)}${byteOffset === 0 ? 'H' : 'L'}`
@@ -289,6 +368,16 @@ function formatRegisterAddressText(address, byteOffset, byteLength, registerType
   return `0x${padHex(address)}`
 }
 
+function formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth) {
+  const byteText = formatRegisterAddressText(address, byteOffset, 1, DEFAULT_REGISTER_TYPE)
+  const startBit = normalizeBitOffset(bitOffset)
+  const endBit = startBit + normalizeBitWidth(bitWidth) - 1
+
+  return endBit === startBit
+    ? `${byteText}.b${startBit}`
+    : `${byteText}.b${startBit}..${endBit}`
+}
+
 function isAddressRangeOverflow(startAddress, wordCount) {
   const address = normalizeAddress(startAddress, 0)
   const count = Math.max(1, Number(wordCount) || 1)
@@ -583,6 +672,81 @@ function bytesToSignedInteger(bytes) {
   return unsignedValue >= signLimit ? unsignedValue - fullRange : unsignedValue
 }
 
+function parseBitFieldValue(register, valueText) {
+  const text = normalizeTextValue(valueText).trim()
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+  let parsed = parseNumberText(text, 'uint32_t')
+  const maxValue = getBitFieldMaxValue(register)
+
+  if (parsed === null && bitWidth === 1) parsed = parseCoilValue(text)
+  if (parsed === null) return null
+  if (Math.round(parsed) !== parsed || parsed < 0 || parsed > maxValue) {
+    throw new Error(`${register.name || '位域'} 超出 0 - ${maxValue} 范围`)
+  }
+
+  return Math.round(parsed)
+}
+
+function decodeBitFieldBytes(register, bytes = []) {
+  const bitOffset = normalizeBitOffset(register.bitOffset)
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+  let byteIndex = 0
+  let currentBitOffset = bitOffset
+  let multiplier = 1
+  let remaining = bitWidth
+  let value = 0
+
+  while (remaining > 0 && byteIndex < bytes.length) {
+    const take = Math.min(8 - currentBitOffset, remaining)
+    const mask = (1 << take) - 1
+    const part = ((Number(bytes[byteIndex]) & 0xFF) >> currentBitOffset) & mask
+
+    value += part * multiplier
+    multiplier *= Math.pow(2, take)
+    remaining -= take
+    byteIndex += 1
+    currentBitOffset = 0
+  }
+
+  return remaining > 0 ? null : value
+}
+
+function encodeBitFieldIntoBytes(register, bytes, byteStart = 0) {
+  const valueText = normalizeTextValue(register.inputValue)
+  let value = parseBitFieldValue(register, valueText)
+  const bitOffset = normalizeBitOffset(register.bitOffset)
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+  let byteIndex = Math.max(0, Math.floor(Number(byteStart) || 0))
+  let currentBitOffset = bitOffset
+  let remaining = bitWidth
+
+  if (value === null) return null
+
+  while (remaining > 0) {
+    const take = Math.min(8 - currentBitOffset, remaining)
+    const mask = (1 << take) - 1
+    const shiftedMask = (mask << currentBitOffset) & 0xFF
+    const part = value & mask
+
+    if (byteIndex >= bytes.length) return null
+    bytes[byteIndex] = ((Number(bytes[byteIndex]) & 0xFF) & (~shiftedMask & 0xFF))
+      | ((part << currentBitOffset) & shiftedMask)
+
+    value = Math.floor(value / Math.pow(2, take))
+    remaining -= take
+    byteIndex += 1
+    currentBitOffset = 0
+  }
+
+  return bytes
+}
+
+function encodeBitFieldBytes(register) {
+  const bytes = Array.from({ length: getBitFieldByteLength(register) }, () => 0)
+
+  return encodeBitFieldIntoBytes(register, bytes, 0)
+}
+
 function getRegisterDataBytes(register, words) {
   const dataType = getDataType(register.dataType).key
   const byteLength = getRegisterByteLength(dataType, register)
@@ -597,6 +761,10 @@ function encodeRegisterBytes(register) {
   const valueText = normalizeTextValue(register.inputValue)
   const byteLength = getRegisterByteLength(dataType, register)
 
+  if (isBitFieldRegister(register)) {
+    return encodeBitFieldBytes(register)
+  }
+
   if (isTextRegister(dataType)) {
     const byteLimit = getEncodeByteLimit(register)
     const bytes = encodeTextBytes(valueText, dataType, byteLimit)
@@ -640,12 +808,16 @@ function encodeRegisterWords(register) {
 function decodeRegisterValue(register, words) {
   const dataType = getDataType(register.dataType).key
 
-  if (!Array.isArray(words) || words.length < getRegisterWordCount(dataType, register)) return null
+  if (!Array.isArray(words) || words.length < getRegisterWordCountAtOffset(dataType, register.byteOffset || 0, register)) return null
 
   const bytes = getRegisterDataBytes(register, words)
   const byteLength = getRegisterByteLength(dataType, register)
   if (bytes.length < byteLength) return null
 
+  if (isBitFieldRegister(register)) {
+    return decodeBitFieldBytes(register, bytes)
+  }
+
   if (isTextRegister(dataType)) {
     return decodeTextBytes(bytes.slice(0, getEncodeByteLimit(register)), dataType)
   }
@@ -704,10 +876,49 @@ function normalizeRegisterDataType(register, registerType) {
   return getDataType(register.dataType || register.type || DEFAULT_DATA_TYPE).key
 }
 
+function pickFields(source, fields) {
+  return fields.reduce((result, field) => {
+    if (source && source[field] !== undefined && source[field] !== null && source[field] !== '') {
+      result[field] = source[field]
+    }
+
+    return result
+  }, {})
+}
+
+function createRegisterSourceMetaText(register) {
+  const bitText = isBitFieldRegister(register)
+    ? `bit${normalizeBitOffset(register.bitOffset)}:${normalizeBitWidth(register.bitWidth)}`
+    : ''
+  const parts = [
+    register.sourceMemoryArea,
+    register.sourceAddressText,
+    bitText,
+    register.sourceSymbolType && register.sourceSymbolType !== '---' ? register.sourceSymbolType : ''
+  ].filter(Boolean)
+
+  return parts.join(' · ')
+}
+
+function createGroupSourceMetaText(group) {
+  const parts = [
+    group.sourceMemoryArea,
+    group.sourceAddressText,
+    group.sourceSymbolName,
+    group.sourceSegmentModule
+  ].filter(Boolean)
+
+  return parts.join(' · ')
+}
+
 function normalizeRegister(register, group, index, address, byteOffset = 0) {
   const registerType = getRegisterType(group.registerType).key
   const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
+  const byteAddressed = isByteAddressedGroup(group)
   const dataType = normalizeRegisterDataType(register, registerType)
+  const bitOffset = normalizeBitOffset(register.bitOffset)
+  const bitWidth = normalizeBitWidth(register.bitWidth)
+  const isBitField = !isBitRegisterType(registerType) && isBitFieldRegister(register)
   const textByteLength = isTextRegister(dataType)
     ? normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
     : ''
@@ -715,15 +926,24 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
   const savedValue = getRegisterSavedValue(register)
   const inputValue = savedValue === null ? defaultValue : savedValue
   const rawValue = register.rawValue === undefined ? null : register.rawValue
-  const byteLength = isBitRegisterType(registerType) ? 1 : getRegisterByteLength(dataType, { layout, textByteLength })
-  const registerCount = isBitRegisterType(registerType) ? 1 : getRegisterWordCountAtOffset(dataType, byteOffset, { layout, textByteLength })
-  const canShowUnit = !isBitRegisterType(registerType) && supportsUnit(dataType)
+  const byteLength = isBitRegisterType(registerType)
+    ? 1
+    : getRegisterByteLength(dataType, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength })
+  const registerCount = isBitRegisterType(registerType)
+    ? 1
+    : getRegisterWordCountAtOffset(dataType, byteOffset, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength })
+  const canShowUnit = !isBitRegisterType(registerType) && !isBitField && supportsUnit(dataType)
   const rawWords = Array.isArray(register.rawWords)
     ? register.rawWords.slice(0, registerCount).map((word) => Number(word) & 0xFFFF)
     : []
+  const rawBytes = Array.isArray(register.rawBytes)
+    ? register.rawBytes.slice(0, byteLength).map((byte) => Number(byte) & 0xFF)
+    : []
   const rawValueText = rawValue === null
     ? '--'
-    : (isBitRegisterType(registerType) ? formatCoilDisplayValue(rawValue) : formatRawWordText(rawWords))
+    : (isBitRegisterType(registerType)
+      ? formatCoilDisplayValue(rawValue)
+      : (byteAddressed ? formatRawByteText(rawBytes) : formatRawWordText(rawWords)))
   const displayValue = rawValue === null
     ? (inputValue.trim() ? inputValue : '--')
     : formatRegisterValue({ ...register, dataType, byteOffset }, rawValue)
@@ -732,12 +952,24 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     address,
     addressRangeText: isBitRegisterType(registerType)
       ? `0x${padHex(address)}`
-      : formatAddressRange(address, registerCount),
-    addressText: formatRegisterAddressText(address, byteOffset, byteLength, registerType),
+      : (byteAddressed
+        ? formatAddressRange(address, Math.max(1, byteLength))
+        : formatAddressRange(address, registerCount)),
+    addressText: isBitField
+      ? (byteAddressed
+        ? `${formatAddressRange(address, Math.max(1, byteLength))}.b${bitOffset}${bitWidth > 1 ? `..b${bitOffset + bitWidth - 1}` : ''}`
+        : formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth))
+      : (byteAddressed
+        ? formatAddressRange(address, Math.max(1, byteLength))
+        : formatRegisterAddressText(address, byteOffset, byteLength, registerType)),
+    bitOffset: isBitField ? bitOffset : '',
+    bitWidth: isBitField ? bitWidth : '',
     byteLength,
     byteLengthText: isBitRegisterType(registerType)
       ? '1bit'
-      : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`),
+      : (isBitField
+        ? (bitWidth === 1 ? `1bit/占${byteLength}B` : `${bitWidth}bit/占${byteLength}B`)
+        : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`)),
     byteStart: Math.max(0, Math.floor(Number(register.byteStart) || 0)),
     dataType,
     dataTypeIndex: getDataTypeIndex(dataType),
@@ -749,23 +981,28 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     inputValue,
     isStructField: layout === GROUP_LAYOUT_STRUCT || !!register.isStructField,
     layout,
+    isBitField,
     isDirty: !!register.isDirty,
     maxValue: normalizeTextValue(register.maxValue),
     minValue: normalizeTextValue(register.minValue),
     name: register.name || `寄存器 ${index + 1}`,
     rawValue,
     rawValueText,
+    rawBytes,
     rawWords,
     registerCount,
     byteOffset,
     registerType,
     showDataType: !isBitRegisterType(registerType),
-    showRange: !isBitRegisterType(registerType) && supportsRange(dataType),
+    showRange: !isBitRegisterType(registerType) && !isBitField && supportsRange(dataType),
     showTextLength: !isBitRegisterType(registerType) && isTextRegister(dataType),
     showUnit: canShowUnit,
     textByteLength,
     unit: canShowUnit ? normalizeTextValue(register.unit).trim() : '',
-    remark: register.remark || ''
+    structByteLength: register.structByteLength,
+    remark: register.remark || '',
+    ...pickFields(register, SOURCE_REGISTER_FIELDS),
+    sourceMetaText: createRegisterSourceMetaText(register)
   }
 }
 
@@ -775,6 +1012,7 @@ function normalizeGroup(group) {
   const maxQuantity = getMaxQuantity(registerType.key)
   const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
   const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
+  const byteAddressed = isByteAddressedGroup(group)
   const hasExplicitQuantity = group.quantity !== undefined && group.quantity !== null && group.quantity !== ''
   const quantity = hasExplicitQuantity
     ? clampInteger(group.quantity, 1, maxQuantity, 1)
@@ -789,7 +1027,8 @@ function normalizeGroup(group) {
     quantity,
     registerType: registerType.key,
     startAddress,
-    touchStartX: 0
+    touchStartX: 0,
+    ...pickFields(group, SOURCE_GROUP_FIELDS)
   }
   const registers = []
   let nextAddress = startAddress
@@ -807,18 +1046,24 @@ function normalizeGroup(group) {
     let byteOffset = 0
 
     if (!isBitRegister) {
-      const byteLength = getRegisterByteLength(dataType, { layout, textByteLength })
+      const explicitByteStart = Number(sourceRegister.byteStart)
+      const hasExplicitByteStart = layout === GROUP_LAYOUT_STRUCT && Number.isFinite(explicitByteStart)
+      const byteLength = getRegisterByteLength(dataType, { ...sourceRegister, layout, textByteLength })
       if (layout !== GROUP_LAYOUT_STRUCT && !isByteRegister(dataType) && nextByteOffset % 2 !== 0) {
         nextByteOffset += 1
       }
 
-      address = startAddress + Math.floor(nextByteOffset / 2)
-      byteOffset = nextByteOffset % 2
+      const currentByteStart = hasExplicitByteStart
+        ? Math.max(0, Math.floor(explicitByteStart))
+        : nextByteOffset
+
+      address = byteAddressed ? startAddress + currentByteStart : startAddress + Math.floor(currentByteStart / 2)
+      byteOffset = byteAddressed ? 0 : currentByteStart % 2
       normalizedSourceRegister = {
         ...sourceRegister,
-        byteStart: nextByteOffset
+        byteStart: currentByteStart
       }
-      nextByteOffset += byteLength
+      nextByteOffset = Math.max(nextByteOffset, currentByteStart + byteLength)
     }
 
     const register = normalizeRegister(normalizedSourceRegister, baseGroup, index, address, byteOffset)
@@ -831,16 +1076,17 @@ function normalizeGroup(group) {
     : Math.max(1, nextByteOffset)
   const paddedByteLength = isBitRegisterType(baseGroup.registerType)
     ? byteLength
-    : alignEvenByteLength(byteLength)
+    : (byteAddressed ? byteLength : alignEvenByteLength(byteLength))
   const wordQuantity = isBitRegisterType(baseGroup.registerType)
     ? Math.max(1, nextAddress - startAddress)
-    : Math.max(1, paddedByteLength / 2)
-  const addressOverflow = isAddressRangeOverflow(startAddress, wordQuantity)
-  const endAddress = startAddress + wordQuantity - 1
+    : (byteAddressed ? Math.max(1, byteLength) : Math.max(1, paddedByteLength / 2))
+  const addressSpan = byteAddressed ? Math.max(1, byteLength) : wordQuantity
+  const addressOverflow = isAddressRangeOverflow(startAddress, addressSpan)
+  const endAddress = startAddress + addressSpan - 1
 
   return {
     ...baseGroup,
-    addressRangeText: formatAddressRange(startAddress, wordQuantity),
+    addressRangeText: formatAddressRange(startAddress, addressSpan),
     addressOverflow,
     addressWarningText: addressOverflow ? '地址超出 0xFFFF' : '',
     endAddressText: addressOverflow ? `0x${padHex(MAX_MODBUS_ADDRESS)}+` : `0x${padHex(endAddress)}`,
@@ -852,6 +1098,7 @@ function normalizeGroup(group) {
     registerTypeText: registerType.label,
     registers,
     paddedByteLength,
+    sourceMetaText: createGroupSourceMetaText(baseGroup),
     startAddressText: `0x${padHex(startAddress)}`,
     wordQuantity,
     writable: registerType.writable
@@ -912,9 +1159,12 @@ function cloneImportedGroup(group) {
       textByteLength: register.textByteLength,
       remark: register.remark,
       unit: register.unit,
-      value: register.value
+      value: register.value,
+      ...pickFields(register, STRUCT_REGISTER_FIELDS),
+      ...pickFields(register, SOURCE_REGISTER_FIELDS)
     })),
-    startAddress: group.startAddress
+    startAddress: group.startAddress,
+    ...pickFields(group, SOURCE_GROUP_FIELDS)
   }
 }
 
@@ -956,6 +1206,36 @@ function getRegisterWordsFromWordCache(register, wordCache) {
   return words
 }
 
+function getRegisterBytesFromByteCache(register, byteCache) {
+  const bytes = []
+  for (let offset = 0; offset < register.byteLength; offset += 1) {
+    const byte = byteCache[register.address + offset]
+    if (byte === undefined) return null
+    bytes.push(Number(byte) & 0xFF)
+  }
+
+  return bytes
+}
+
+function getRegisterWordsFromByteCache(register, byteCache) {
+  const bytes = getRegisterBytesFromByteCache(register, byteCache)
+  if (!bytes) return null
+
+  return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
+}
+
+function decodeRegisterFromBytes(register, bytes) {
+  if (!Array.isArray(bytes) || bytes.length < register.byteLength) return null
+
+  return decodeRegisterValue(
+    {
+      ...register,
+      byteOffset: 0
+    },
+    bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
+  )
+}
+
 function decodeRegisterFromWordCache(register, wordCache) {
   const words = getRegisterWordsFromWordCache(register, wordCache)
   if (!words) return null
@@ -963,6 +1243,13 @@ function decodeRegisterFromWordCache(register, wordCache) {
   return decodeRegisterValue(register, words)
 }
 
+function decodeRegisterFromByteCache(register, byteCache) {
+  const bytes = getRegisterBytesFromByteCache(register, byteCache)
+  if (!bytes) return null
+
+  return decodeRegisterFromBytes(register, bytes)
+}
+
 function getRegisterEncodedWords(register) {
   return encodeRegisterWords({
     ...register,
@@ -970,6 +1257,13 @@ function getRegisterEncodedWords(register) {
   })
 }
 
+function getRegisterEncodedBytes(register) {
+  return encodeRegisterBytes({
+    ...register,
+    inputValue: getRegisterWriteValueText(register)
+  })
+}
+
 function getRegisterByteStart(register, groupStartAddress = 0) {
   if (Number.isFinite(Number(register.byteStart))) {
     return Math.max(0, Math.floor(Number(register.byteStart)))
@@ -978,18 +1272,41 @@ function getRegisterByteStart(register, groupStartAddress = 0) {
   return Math.max(0, ((Number(register.address) || 0) - (Number(groupStartAddress) || 0)) * 2 + (Number(register.byteOffset) || 0))
 }
 
-function getGroupEncodedWords(group) {
+function getGroupEncodedBytes(group, baseBytes = null) {
   const byteLength = Math.max(2, Number(group && group.paddedByteLength) || ((Number(group && group.wordQuantity) || 1) * 2))
-  const bytes = Array.from({ length: byteLength }, () => 0)
+  const bytes = Array.isArray(baseBytes)
+    ? baseBytes.slice(0, byteLength).map((byte) => Number(byte) & 0xFF)
+    : []
   const registers = Array.isArray(group && group.registers) ? group.registers : []
 
+  while (bytes.length < byteLength) {
+    bytes.push(0)
+  }
+
   registers.forEach((register) => {
+    const byteStart = getRegisterByteStart(register, group.startAddress)
+
+    if (isBitFieldRegister(register)) {
+      const encodedBytes = encodeBitFieldIntoBytes(
+        {
+          ...register,
+          inputValue: getRegisterWriteValueText(register)
+        },
+        bytes,
+        byteStart
+      )
+
+      if (!Array.isArray(encodedBytes)) {
+        throw new Error(`${register.name || '位域'} 没有有效写入值`)
+      }
+      return
+    }
+
     const registerBytes = encodeRegisterBytes(register)
     if (!Array.isArray(registerBytes) || !registerBytes.length) {
       throw new Error(`${register.name || '寄存器'} 没有有效写入值`)
     }
 
-    const byteStart = getRegisterByteStart(register, group.startAddress)
     for (let offset = 0; offset < register.byteLength; offset += 1) {
       if (byteStart + offset < bytes.length) {
         bytes[byteStart + offset] = Number(registerBytes[offset] || 0) & 0xFF
@@ -997,7 +1314,11 @@ function getGroupEncodedWords(group) {
     }
   })
 
-  return bytesToWords(bytes)
+  return bytes
+}
+
+function getGroupEncodedWords(group, baseBytes = null) {
+  return bytesToWords(getGroupEncodedBytes(group, baseBytes))
 }
 
 function validateRegisterValue(register, value) {
@@ -1013,6 +1334,13 @@ function validateRegisterValue(register, value) {
     return true
   }
 
+  if (isBitFieldRegister(register)) {
+    if (parseBitFieldValue(register, valueText) === null) {
+      throw new Error(`${register.name || '位域'} 输入值无效`)
+    }
+    return true
+  }
+
   const dataType = getDataType(register.dataType).key
   if (isTextRegister(dataType)) {
     encodeTextBytes(valueText, dataType, getEncodeByteLimit(register))
@@ -1040,15 +1368,20 @@ module.exports = {
   MAX_MODBUS_ADDRESS,
   REGISTER_TYPE_OPTIONS,
   cloneImportedGroup,
+  decodeRegisterFromByteCache,
   decodeRegisterFromWordCache,
   decodeRegisterValue,
   formatCoilDisplayValue,
   formatRegisterValue,
   getDataType,
+  getRegisterEncodedBytes,
   getRegisterEncodedWords,
+  getGroupEncodedBytes,
   getGroupEncodedWords,
   getRegisterWordCount,
   getRegisterJsonValue,
+  getRegisterBytesFromByteCache,
+  getRegisterWordsFromByteCache,
   getRegisterWordsFromWordCache,
   getRegisterWriteValueText,
   isAddressRangeOverflow,

+ 257 - 5
domain/generic-modbus/struct-parser.js

@@ -44,6 +44,24 @@ const STRUCT_PATTERNS = [
   /typedef\s+struct(?:\s+[A-Za-z_]\w*)?\s*\{([\s\S]*?)\}\s*([A-Za-z_]\w*)\s*;/g,
   /struct\s+([A-Za-z_]\w*)\s*\{([\s\S]*?)\}\s*;/g
 ]
+const STRUCT_VARIABLE_QUALIFIERS = {
+  code: true,
+  const: true,
+  data: true,
+  extern: true,
+  idata: true,
+  pdata: true,
+  static: true,
+  volatile: true,
+  xdata: true
+}
+
+function normalizeLookupName(value) {
+  return String(value || '')
+    .replace(/^_+/, '')
+    .replace(/[^A-Za-z0-9]/g, '')
+    .toLowerCase()
+}
 
 function stripComments(source) {
   return String(source || '')
@@ -79,6 +97,15 @@ function createAliasMap(source) {
     if (resolvedType) aliases[typedefMatch[2]] = resolvedType
   }
 
+  const typedefStructPattern = /typedef\s+struct(?:\s+([A-Za-z_]\w*))?\s*\{[\s\S]*?\}\s*([A-Za-z_]\w*)\s*;/g
+  let structTypedefMatch
+
+  while ((structTypedefMatch = typedefStructPattern.exec(source))) {
+    const tagName = structTypedefMatch[1]
+    const typedefName = structTypedefMatch[2]
+    if (tagName && typedefName) aliases[`struct ${tagName}`] = typedefName
+  }
+
   return aliases
 }
 
@@ -128,6 +155,40 @@ function findStruct(source) {
   return null
 }
 
+function findStructs(source) {
+  const structs = []
+
+  STRUCT_PATTERNS.forEach((pattern) => {
+    pattern.lastIndex = 0
+    let match
+
+    while ((match = pattern.exec(source))) {
+      if (pattern === STRUCT_PATTERNS[0]) {
+        structs.push({
+          body: match[1],
+          name: match[2]
+        })
+      } else {
+        structs.push({
+          body: match[2],
+          name: match[1],
+          tagName: match[1]
+        })
+      }
+    }
+  })
+
+  const seen = {}
+
+  return structs.filter((item) => {
+    const key = item.name
+    if (!key || seen[key]) return false
+    seen[key] = true
+
+    return true
+  })
+}
+
 function parseArrayDimensions(suffix) {
   const dimensions = []
   const pattern = /\[([^\]]*)\]/g
@@ -160,7 +221,7 @@ function splitDeclarators(statement) {
 }
 
 function parseFirstDeclarator(text) {
-  const match = String(text || '').match(/^(.+?)\s+(\**\s*[A-Za-z_]\w*(?:\s*\[[^\]]*\])*(?:\s*:\s*\d+)?)$/)
+  const match = String(text || '').match(/^(.+?)\s+(\**\s*(?:[A-Za-z_]\w*)?(?:\s*\[[^\]]*\])*(?:\s*:\s*\d+)?)$/)
   if (!match) return null
 
   return {
@@ -170,16 +231,28 @@ function parseFirstDeclarator(text) {
 }
 
 function parseDeclarator(text) {
-  const cleaned = String(text || '')
+  const rawText = String(text || '')
+  const bitWidthMatch = rawText.match(/:\s*(\d+)\s*$/)
+  const cleaned = rawText
     .replace(/=.*/, '')
     .replace(/:\s*\d+\s*$/, '')
     .replace(/\*/g, '')
     .trim()
+
+  if (!cleaned && bitWidthMatch) {
+    return {
+      arrayDimensions: [],
+      bitWidth: Number(bitWidthMatch[1]),
+      name: ''
+    }
+  }
+
   const match = cleaned.match(/^([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)$/)
   if (!match) return null
 
   return {
     arrayDimensions: parseArrayDimensions(match[2]),
+    bitWidth: bitWidthMatch ? Number(bitWidthMatch[1]) : null,
     name: match[1]
   }
 }
@@ -193,12 +266,97 @@ function isAsciiArray(typeText, dataType, name, arrayLength) {
   return dataType === 'uint8_t' && /(^|_)(model|name|text|str|string|chip|version|ver|serial|sn)($|_)/i.test(name)
 }
 
-function createRegisterFromField(field, dataType, originalTypeText) {
+function getDataTypeByteLength(dataType) {
+  if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
+  if (dataType === 'int8_t' || dataType === 'uint8_t') return 1
+
+  return 2
+}
+
+function getBitFieldDataType(bitWidth) {
+  const width = Math.max(1, Math.round(Number(bitWidth) || 1))
+  if (width <= 8) return 'uint8_t'
+  if (width <= 16) return 'uint16_t'
+
+  return 'uint32_t'
+}
+
+function isBitType(typeText) {
+  return normalizeTypeText(typeText).toLowerCase() === 'bit'
+}
+
+function alignLayoutToByte(layoutState) {
+  if (layoutState.bitOffset % 8 !== 0) {
+    layoutState.bitOffset += 8 - (layoutState.bitOffset % 8)
+  }
+}
+
+function getLayoutByteStart(layoutState) {
+  return Math.floor(layoutState.bitOffset / 8)
+}
+
+function advanceLayoutBytes(layoutState, byteLength) {
+  layoutState.bitOffset += Math.max(1, Number(byteLength) || 1) * 8
+}
+
+function createBitFieldRegister(field, bitWidth, layoutState, name) {
+  const width = Math.max(0, Math.round(Number(bitWidth) || 0))
+
+  if (width === 0) {
+    alignLayoutToByte(layoutState)
+    return []
+  }
+
+  const byteStart = getLayoutByteStart(layoutState)
+  const bitOffset = layoutState.bitOffset % 8
+  layoutState.bitOffset += width
+
+  if (!name) return []
+
+  return [{
+    bitOffset,
+    bitWidth: width,
+    byteStart,
+    dataType: getBitFieldDataType(width),
+    isBitField: true,
+    name,
+    unit: 'bit'
+  }]
+}
+
+function createRegisterFromField(field, dataType, originalTypeText, layoutState) {
   const arrayLength = field.arrayDimensions.reduce((total, value) => total * value, 1)
   const hasArray = field.arrayDimensions.length > 0
+  const bitFieldWidth = field.bitWidth !== null && field.bitWidth !== undefined
+    ? field.bitWidth
+    : (isBitType(originalTypeText) ? 1 : null)
+
+  if (bitFieldWidth !== null && bitFieldWidth !== undefined) {
+    if (hasArray) {
+      const registers = []
+      for (let index = 0; index < arrayLength; index += 1) {
+        registers.push(...createBitFieldRegister(
+          field,
+          bitFieldWidth,
+          layoutState,
+          field.name ? `${field.name}[${index}]` : ''
+        ))
+      }
+
+      return registers
+    }
+
+    return createBitFieldRegister(field, bitFieldWidth, layoutState, field.name)
+  }
+
+  alignLayoutToByte(layoutState)
 
   if (hasArray && isAsciiArray(originalTypeText, dataType, field.name, arrayLength)) {
+    const byteStart = getLayoutByteStart(layoutState)
+    advanceLayoutBytes(layoutState, arrayLength)
+
     return [{
+      byteStart,
       dataType: 'ascii',
       name: field.name,
       textByteLength: String(arrayLength)
@@ -206,7 +364,11 @@ function createRegisterFromField(field, dataType, originalTypeText) {
   }
 
   if (!hasArray) {
+    const byteStart = getLayoutByteStart(layoutState)
+    advanceLayoutBytes(layoutState, getDataTypeByteLength(dataType))
+
     return [{
+      byteStart,
       dataType,
       name: field.name
     }]
@@ -214,7 +376,10 @@ function createRegisterFromField(field, dataType, originalTypeText) {
 
   const registers = []
   for (let index = 0; index < arrayLength; index += 1) {
+    const byteStart = getLayoutByteStart(layoutState)
+    advanceLayoutBytes(layoutState, getDataTypeByteLength(dataType))
     registers.push({
+      byteStart,
       dataType,
       name: `${field.name}[${index}]`
     })
@@ -225,6 +390,9 @@ function createRegisterFromField(field, dataType, originalTypeText) {
 
 function parseStructFields(body, aliases) {
   const registers = []
+  const layoutState = {
+    bitOffset: 0
+  }
   const declarations = splitDeclarations(body)
 
   declarations.forEach((statement) => {
@@ -244,11 +412,14 @@ function parseStructFields(body, aliases) {
       const field = parseDeclarator(declaratorText)
       if (!field) return
 
-      registers.push(...createRegisterFromField(field, dataType, first.typeText))
+      registers.push(...createRegisterFromField(field, dataType, first.typeText, layoutState))
     })
   })
 
-  return registers
+  return registers.map((register) => ({
+    ...register,
+    structByteLength: Math.ceil(layoutState.bitOffset / 8)
+  }))
 }
 
 function parseStructDefinition(sourceText) {
@@ -271,7 +442,88 @@ function parseStructDefinition(sourceText) {
   }
 }
 
+function getStructNameAliases(structInfo, aliases) {
+  const names = [structInfo.name]
+
+  if (structInfo.tagName) names.push(`struct ${structInfo.tagName}`)
+
+  Object.keys(aliases || {}).forEach((aliasName) => {
+    if (aliases[aliasName] === structInfo.name) names.push(aliasName)
+  })
+
+  return names.filter(Boolean)
+}
+
+function normalizeVariableTypeText(typeText, aliases) {
+  const normalized = normalizeTypeText(typeText)
+  if (!normalized) return ''
+
+  const tokens = normalized
+    .split(/\s+/)
+    .filter((token) => !STRUCT_VARIABLE_QUALIFIERS[token])
+
+  const compact = tokens.join(' ')
+  if (aliases && aliases[compact]) return aliases[compact]
+
+  return compact
+}
+
+function parseStructVariables(source, structs, aliases) {
+  const variablesByName = {}
+
+  structs.forEach((structInfo) => {
+    const structNames = getStructNameAliases(structInfo, aliases)
+
+    structNames.forEach((structName) => {
+      const escaped = structName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+      const variablePattern = new RegExp(`(^|[;\\n{}])\\s*([A-Za-z_][\\w\\s]*?\\s+)?${escaped}\\s+([^;{}()]+);`, 'g')
+      let match
+
+      while ((match = variablePattern.exec(source))) {
+        const prefix = normalizeVariableTypeText(match[2] || '', aliases)
+        if (prefix) continue
+
+        splitDeclarators(match[3]).forEach((declaratorText) => {
+          const field = parseDeclarator(declaratorText)
+          if (!field) return
+
+          const variableInfo = {
+            arrayDimensions: field.arrayDimensions,
+            name: field.name,
+            registers: structInfo.registers,
+            structName: structInfo.name
+          }
+          variablesByName[field.name] = variableInfo
+          variablesByName[field.name.replace(/^_+/, '').toLowerCase()] = variableInfo
+          variablesByName[normalizeLookupName(field.name)] = variableInfo
+        })
+      }
+    })
+  })
+
+  return variablesByName
+}
+
+function parseStructCatalog(sourceText) {
+  const source = stripComments(sourceText)
+  const aliases = createAliasMap(source)
+  const structs = findStructs(source).map((structInfo) => ({
+    ...structInfo,
+    registers: parseStructFields(structInfo.body, aliases)
+  })).filter((structInfo) => structInfo.registers.length)
+
+  if (!structs.length) {
+    throw new Error('未找到可识别的结构体定义')
+  }
+
+  return {
+    structs,
+    variablesByName: parseStructVariables(source, structs, aliases)
+  }
+}
+
 module.exports = {
+  parseStructCatalog,
   parseStructDefinition,
   stripComments
 }

+ 0 - 127
domain/motor-control/calculation-context.js

@@ -1,127 +0,0 @@
-const SCALE_MAX = 32767
-const ATT_COEF = 0.85
-const TWO_PI = 2 * 3.1415926
-
-const DEFAULT_DRIVER_PARAMS = {
-  carrierFrequencyKHz: 16,
-  baseVoltage: 5.0,
-  opAmpGain: 4,
-  samplingResistorMohm: 100,
-  busVoltageDividerRatio: 996.8 / 6.8,
-  analogInputDividerRatio: 200 / 268
-}
-
-let currentDriverParams = {
-  ...DEFAULT_DRIVER_PARAMS
-}
-
-const sharedInputValues = {}
-
-const FAULT_CODE_MAP = {
-  0x01: '硬件过流',
-  0x02: '软件过流',
-  0x03: '风扇过流',
-  0x04: '电流偏置校准失败',
-  0x05: '缺相',
-  0x06: '驱动器上桥短路',
-  0x07: '驱动器下桥短路',
-  0x08: '相间短路',
-  0x09: '启动堵转',
-  0x0A: '运行堵转',
-  0x0B: '过功率',
-  0x0C: '过压',
-  0x0D: '欠压',
-  0x0E: '芯片欠压',
-  0x0F: 'NTC过温',
-  0x10: '电机过温',
-  0x11: 'IPM过温',
-  0x12: '芯片过温',
-  0x13: '串口丢失',
-  0x14: 'PWM丢失'
-}
-
-function toFiniteNumber(value, fallback = 0) {
-  if (typeof value === 'string') {
-    const text = value.trim()
-    const directValue = Number(text)
-    if (Number.isFinite(directValue)) return directValue
-
-    const match = text.match(/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i)
-    const textValue = match ? Number(match[0]) : NaN
-
-    return Number.isFinite(textValue) ? textValue : fallback
-  }
-
-  const numberValue = Number(value)
-
-  return Number.isFinite(numberValue) ? numberValue : fallback
-}
-
-function getSharedInputValues() {
-  return {
-    ...sharedInputValues
-  }
-}
-
-function getDriverParams() {
-  return {
-    ...currentDriverParams
-  }
-}
-
-function updateDriverParams(params = {}) {
-  currentDriverParams = {
-    ...currentDriverParams,
-    ...Object.keys(params).reduce((result, key) => {
-      const value = toFiniteNumber(params[key], NaN)
-      if (Number.isFinite(value) && value > 0) {
-        result[key] = value
-      }
-      return result
-    }, {})
-  }
-}
-
-function getSharedInputDefault() {
-  return 0
-}
-
-function setSharedInputValue(name, value, fallback = getSharedInputDefault(name)) {
-  sharedInputValues[name] = toFiniteNumber(value, fallback)
-}
-
-function setSharedInputValues(registers) {
-  registers.forEach((item) => {
-    setSharedInputValue(item.name, item.inputValue, getSharedInputDefault(item.name))
-  })
-}
-
-function mergeInputValues(registers = []) {
-  return registers.reduce((result, item) => {
-    const fallback = Object.prototype.hasOwnProperty.call(result, item.name) ? result[item.name] : getSharedInputDefault(item.name)
-    result[item.name] = toFiniteNumber(item.inputValue, fallback)
-    return result
-  }, getSharedInputValues())
-}
-
-function getFaultText(code) {
-  const numberValue = Number(code)
-  if (!Number.isFinite(numberValue) || numberValue === 0) return '无故障'
-
-  return FAULT_CODE_MAP[numberValue] || '未知故障'
-}
-
-module.exports = {
-  ATT_COEF,
-  DEFAULT_DRIVER_PARAMS,
-  SCALE_MAX,
-  TWO_PI,
-  getFaultText,
-  getDriverParams,
-  getSharedInputValues,
-  getSharedInputDefault,
-  mergeInputValues,
-  setSharedInputValues,
-  updateDriverParams,
-  toFiniteNumber
-}

+ 0 - 483
domain/motor-control/control-state.js

@@ -1,483 +0,0 @@
-const {
-  controlButtonRegisters,
-  motorParameterInputRegisters,
-  readonlyParamRegisters,
-  speedCommandRegister,
-  statusRegisters
-} = require('./registers.js')
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const {
-  getSharedInputDefault,
-  mergeInputValues,
-  setSharedInputValues,
-  updateDriverParams,
-  toFiniteNumber
-} = require('./calculation-context.js')
-const {
-  calculateParameterInputWriteValue,
-  calculateSpeedCommandWriteValue,
-  SCALE_MAX,
-  formatFixedValue
-} = require('./conversions.js')
-const {
-  updateStatusRegisterWords
-} = require('./status-format.js')
-const {
-  floatToWords,
-  getRegisterWordCache,
-  toRegisterWord,
-  toAddressKey,
-  wordsToFloat
-} = require('../../utils/register-value-utils.js')
-const {
-  appendInputUnit
-} = require('./input-value-utils.js')
-
-const AUTO_READ_MIN_INTERVAL = 100
-const AUTO_READ_MAX_INTERVAL = 3000
-const DEFAULT_AUTO_READ_INTERVAL = 100
-const MOTOR_PARAM_START_ADDRESS = 0x60
-const MOTOR_PARAM_WORD_COUNT = 8
-const DRIVER_PARAM_START_ADDRESS = 0xA0
-const STATUS_START_ADDRESS = 0xC0
-const USER_STATUS_START_ADDRESS = 0xD3
-const MAX_USER_STATUS_COUNT = statusRegisters.filter((item) => item.name.indexOf('用户状态字') === 0).length
-const DEFAULT_USER_STATUS_COUNT = 0
-
-function getRegisterSpanWordCount(registers, startAddress) {
-  const endAddress = registers.reduce((maxAddress, item) => {
-    const address = parseHexInteger(item.address)
-    if (!Number.isFinite(address)) return maxAddress
-
-    return Math.max(maxAddress, address + (item.registerCount || 1))
-  }, startAddress)
-
-  return endAddress - startAddress
-}
-
-const DRIVER_PARAM_WORD_COUNT = getRegisterSpanWordCount(readonlyParamRegisters, DRIVER_PARAM_START_ADDRESS)
-const BASE_STATUS_WORD_COUNT = USER_STATUS_START_ADDRESS - STATUS_START_ADDRESS
-const DRIVER_SUMMARY_REGISTER_NAMES = [
-  '芯片型号',
-  '全区 Flash 校验码',
-  '型号'
-]
-
-function isDriverSummaryRegister(item) {
-  return DRIVER_SUMMARY_REGISTER_NAMES.includes(item.name)
-}
-
-function getDriverReadonlyParamRegisters(registers = readonlyParamRegisters) {
-  return registers.filter((item) => !isDriverSummaryRegister(item))
-}
-
-function getInputValues(registers) {
-  return registers.reduce((result, item) => {
-    result[item.name] = toFiniteNumber(item.inputValue, getSharedInputDefault(item.name))
-    return result
-  }, {})
-}
-
-function updateMotorWriteValues(registers) {
-  const inputValues = getInputValues(registers)
-
-  return registers.map((item) => ({
-    ...item,
-    writeValue: calculateParameterInputWriteValue(item, item.inputValue, inputValues)
-  }))
-}
-
-function formatReadInputValue(item, value) {
-  if (!Number.isFinite(value)) return ''
-  if (item.name === 'LD' || item.name === 'LQ') return formatFixedValue(value, 6)
-  if (item.name === 'RS') return formatFixedValue(value, 4)
-  if (item.type === 'float') return formatFixedValue(value, 2)
-
-  return String(Math.round(value))
-}
-
-function formatHexWord(value) {
-  return `0x${(Number(value) & 0xFFFF).toString(16).toUpperCase().padStart(4, '0')}`
-}
-
-function wordsToAscii(words, startIndex, byteLength) {
-  const chars = []
-  const wordCount = Math.ceil(byteLength / 2)
-
-  for (let index = 0; index < wordCount; index += 1) {
-    const word = Number(words[startIndex + index])
-    if (!Number.isInteger(word)) break
-
-    const bytes = [(word >> 8) & 0xFF, word & 0xFF]
-
-    for (const byte of bytes) {
-      if (chars.length >= byteLength || byte === 0) {
-        return chars.join('').trim() || '--'
-      }
-
-      if (byte >= 0x20 && byte <= 0x7E) {
-        chars.push(String.fromCharCode(byte))
-      }
-    }
-  }
-
-  return chars.join('').trim() || '--'
-}
-
-function clampNumber(value, minValue, maxValue, fallback) {
-  const numberValue = toFiniteNumber(value, NaN)
-  if (!Number.isFinite(numberValue)) return fallback
-
-  return Math.min(Math.max(Math.round(numberValue), minValue), maxValue)
-}
-
-function getUserStatusCount(value) {
-  return clampNumber(value, 0, MAX_USER_STATUS_COUNT, DEFAULT_USER_STATUS_COUNT)
-}
-
-function getStatusWordCount(userStatusCount = DEFAULT_USER_STATUS_COUNT) {
-  return BASE_STATUS_WORD_COUNT + getUserStatusCount(userStatusCount)
-}
-
-function cloneRegister(item) {
-  return {
-    ...item
-  }
-}
-
-function createInitialState() {
-  return {
-    autoReadInterval: DEFAULT_AUTO_READ_INTERVAL,
-    autoReadStatus: false,
-    connectedDevice: null,
-    controlActionButtons: controlButtonRegisters.filter((item) => item.momentary).map(cloneRegister),
-    controlButtons: controlButtonRegisters.filter((item) => !item.momentary).map(cloneRegister),
-    errorText: '',
-    isAwaitingResponse: false,
-    isReadingDriver: false,
-    isReadingMotor: false,
-    isSending: false,
-    isWritingMotor: false,
-    chipModel: '--',
-    flashChecksum: '--',
-    motorModel: '--',
-    motorParameterInputRegisters,
-    readonlyParamRegisters: getDriverReadonlyParamRegisters(),
-    speedCommand: speedCommandRegister,
-    systemTip: '',
-    userStatusCount: DEFAULT_USER_STATUS_COUNT
-  }
-}
-
-function applyTransportState(data, transportState) {
-  const nextState = {
-    connectedDevice: transportState.connectedDevice,
-    errorText: transportState.errorText,
-    isAwaitingResponse: transportState.isAwaitingResponse,
-    isSending: transportState.isSending,
-    systemTip: transportState.systemTip
-  }
-
-  if (!transportState.connectedDevice && data.autoReadStatus) {
-    nextState.autoReadStatus = false
-  }
-
-  if (!transportState.connectedDevice) {
-    nextState.isReadingDriver = false
-    nextState.isReadingMotor = false
-    nextState.isWritingMotor = false
-  }
-
-  return nextState
-}
-
-function applyMotorParameterInput(data, index, value) {
-  const changedRegisters = data.motorParameterInputRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value
-    }
-  })
-  const nextRegisters = updateMotorWriteValues(changedRegisters)
-  const inputValues = mergeInputValues(nextRegisters)
-
-  setSharedInputValues(nextRegisters)
-
-  return {
-    motorParameterInputRegisters: nextRegisters,
-    speedCommand: {
-      ...data.speedCommand,
-      isDirty: true,
-      writeValue: calculateSpeedCommandWriteValue(data.speedCommand.inputValue, inputValues)
-    }
-  }
-}
-
-function applySpeedCommandInput(data, inputValue) {
-  const inputValues = mergeInputValues(data.motorParameterInputRegisters)
-
-  return {
-    speedCommand: {
-      ...data.speedCommand,
-      isDirty: true,
-      inputValue,
-      writeValue: calculateSpeedCommandWriteValue(inputValue, inputValues)
-    }
-  }
-}
-
-function applySpeedCommandReadValue(data, rawValue) {
-  const wordValue = Number(rawValue)
-  if (!Number.isInteger(wordValue)) return {}
-
-  const inputValues = mergeInputValues(data.motorParameterInputRegisters)
-  const speedBase = toFiniteNumber(inputValues['速度基准'])
-  const inputValue = speedBase > 0
-    ? appendInputUnit(data.speedCommand, formatFixedValue(wordValue / SCALE_MAX * speedBase, 2))
-    : data.speedCommand.inputValue
-
-  return {
-    speedCommand: {
-      ...data.speedCommand,
-      isDirty: false,
-      inputValue,
-      writeValue: String(wordValue & 0xFFFF)
-    }
-  }
-}
-
-function getControlButtonWriteValue(button) {
-  if (!button) return 0
-
-  return button.writeValue
-}
-
-function getNextControlButton(button) {
-  if (button.momentary) return button
-
-  return {
-    ...button,
-    name: button.nextName,
-    nextName: button.name,
-    nextWriteValue: button.writeValue,
-    writeValue: button.nextWriteValue
-  }
-}
-
-function applyControlSuccess(data, button) {
-  if (!button) return {}
-
-  if (button.momentary) {
-    return {
-      systemTip: `${button.name}已下发`
-    }
-  }
-
-  return {
-    controlButtons: data.controlButtons.map((item) => (
-      item.key === button.key ? getNextControlButton(item) : item
-    )),
-    systemTip: `${button.name}已下发`
-  }
-}
-
-function getControlButtonFromRead(button, value) {
-  if (!button || button.momentary) return button
-
-  const readValue = Number(value)
-  if (!Number.isFinite(readValue)) return button
-  if (Number(button.writeValue) === readValue) return getNextControlButton(button)
-  if (Number(button.nextWriteValue) === readValue) return button
-
-  return button
-}
-
-function applyControlReadValues(data, coilValues = {}) {
-  return {
-    controlButtons: data.controlButtons.map((item) => {
-      const value = coilValues[toAddressKey(item.address)]
-
-      return value === undefined ? item : getControlButtonFromRead(item, value)
-    })
-  }
-}
-
-function buildMotorMainWriteValues(data) {
-  const registerMap = data.motorParameterInputRegisters.reduce((result, item) => {
-    result[item.name] = item
-    return result
-  }, {})
-  const ldWords = floatToWords(registerMap.LD && registerMap.LD.inputValue)
-  const lqWords = floatToWords(registerMap.LQ && registerMap.LQ.inputValue)
-  const rsWords = floatToWords(registerMap.RS && registerMap.RS.inputValue)
-  const polePairsWord = toRegisterWord(registerMap['极对数'] && registerMap['极对数'].inputValue)
-  const speedBaseWord = toRegisterWord(registerMap['速度基准'] && registerMap['速度基准'].inputValue)
-
-  if (!ldWords || !lqWords || !rsWords || !Number.isInteger(polePairsWord) || !Number.isInteger(speedBaseWord)) {
-    return {
-      errorText: '请检查 LD、LQ、RS、极对数和速度基准的输入值',
-      values: null
-    }
-  }
-
-  return {
-    errorText: '',
-    values: ldWords.concat(lqWords, rsWords, [polePairsWord, speedBaseWord])
-  }
-}
-
-function applyMotorParameterReadValues(data, registerWordCache) {
-  const nextRegisters = data.motorParameterInputRegisters.map((item) => {
-    let readValue = null
-
-    if (item.name === 'LD' && registerWordCache[0x60] !== undefined && registerWordCache[0x61] !== undefined) {
-      readValue = wordsToFloat(registerWordCache[0x60], registerWordCache[0x61])
-    } else if (item.name === 'LQ' && registerWordCache[0x62] !== undefined && registerWordCache[0x63] !== undefined) {
-      readValue = wordsToFloat(registerWordCache[0x62], registerWordCache[0x63])
-    } else if (item.name === 'RS' && registerWordCache[0x64] !== undefined && registerWordCache[0x65] !== undefined) {
-      readValue = wordsToFloat(registerWordCache[0x64], registerWordCache[0x65])
-    } else if (item.name === '极对数' && registerWordCache[0x66] !== undefined) {
-      readValue = registerWordCache[0x66]
-    } else if (item.name === '速度基准' && registerWordCache[0x67] !== undefined) {
-      readValue = registerWordCache[0x67]
-    }
-
-    if (readValue === null) return item
-
-    return {
-      ...item,
-      isDirty: false,
-      inputValue: appendInputUnit(item, formatReadInputValue(item, readValue))
-    }
-  })
-  const nextWriteRegisters = updateMotorWriteValues(nextRegisters)
-  const inputValues = mergeInputValues(nextWriteRegisters)
-
-  setSharedInputValues(nextWriteRegisters)
-
-  return {
-    motorParameterInputRegisters: nextWriteRegisters,
-    speedCommand: {
-      ...data.speedCommand,
-      writeValue: calculateSpeedCommandWriteValue(data.speedCommand.inputValue, inputValues)
-    }
-  }
-}
-
-function clearMotorParameterDirty(data) {
-  return {
-    motorParameterInputRegisters: data.motorParameterInputRegisters.map((item) => ({
-      ...item,
-      isDirty: false
-    }))
-  }
-}
-
-function clearSpeedCommandDirty(data) {
-  return {
-    speedCommand: {
-      ...data.speedCommand,
-      isDirty: false
-    }
-  }
-}
-
-function applyMotorParameterBlur(data, index, value) {
-  const item = data.motorParameterInputRegisters[index]
-  if (!item) return {}
-
-  return applyMotorParameterInput(data, index, appendInputUnit(item, value === undefined ? item.inputValue : value))
-}
-
-function applySpeedCommandBlur(data, value) {
-  return applySpeedCommandInput(
-    data,
-    appendInputUnit(data.speedCommand, value === undefined ? data.speedCommand.inputValue : value)
-  )
-}
-
-function applyDriverParameterReadValues(data, words) {
-  if (!Array.isArray(words) || words.length < DRIVER_PARAM_WORD_COUNT) return {}
-
-  const carrierFrequencyKHz = (words[0] >> 8) & 0xFF
-  const baseVoltage = (words[0] & 0xFF) / 10
-  const opAmpGain = words[1] & 0xFFFF
-  const samplingResistorMohm = words[2] & 0xFFFF
-  const busVoltageDividerRatio = wordsToFloat(words[4], words[5])
-  const analogInputDividerRatio = wordsToFloat(words[6], words[7])
-  const displayValues = {
-    芯片型号: wordsToAscii(words, 8, 8),
-    型号: wordsToAscii(words, 12, 16),
-    载波频率: String(carrierFrequencyKHz),
-    基准电压: formatFixedValue(baseVoltage, 2),
-    运放倍数: String(opAmpGain),
-    采样电阻: String(samplingResistorMohm),
-    '全区 Flash 校验码': formatHexWord(words[3]),
-    母线电压分压比: formatFixedValue(busVoltageDividerRatio, 2),
-    模拟输入电压分压比: formatFixedValue(analogInputDividerRatio, 2)
-  }
-
-  updateDriverParams({
-    analogInputDividerRatio,
-    baseVoltage,
-    busVoltageDividerRatio,
-    carrierFrequencyKHz,
-    opAmpGain,
-    samplingResistorMohm
-  })
-
-  return {
-    chipModel: displayValues['芯片型号'],
-    flashChecksum: displayValues['全区 Flash 校验码'],
-    motorModel: displayValues['型号'],
-    readonlyParamRegisters: getDriverReadonlyParamRegisters(data.readonlyParamRegisters).map((item) => ({
-      ...item,
-      displayValue: displayValues[item.name] || item.displayValue || '--'
-    }))
-  }
-}
-
-function applyStatusReadValues(words, startAddress = STATUS_START_ADDRESS) {
-  if (!Array.isArray(words) || !words.length) return {}
-
-  updateStatusRegisterWords(statusRegisters, startAddress, words)
-
-  return {}
-}
-
-module.exports = {
-  AUTO_READ_MAX_INTERVAL,
-  AUTO_READ_MIN_INTERVAL,
-  DRIVER_PARAM_START_ADDRESS,
-  DRIVER_PARAM_WORD_COUNT,
-  MAX_USER_STATUS_COUNT,
-  MOTOR_PARAM_START_ADDRESS,
-  MOTOR_PARAM_WORD_COUNT,
-  STATUS_START_ADDRESS,
-  applyControlReadValues,
-  applyControlSuccess,
-  applyDriverParameterReadValues,
-  applyMotorParameterBlur,
-  applyMotorParameterInput,
-  applyMotorParameterReadValues,
-  clearMotorParameterDirty,
-  clearSpeedCommandDirty,
-  applySpeedCommandBlur,
-  applySpeedCommandInput,
-  applySpeedCommandReadValue,
-  applyStatusReadValues,
-  applyTransportState,
-  buildMotorMainWriteValues,
-  clampNumber,
-  createInitialState,
-  getStatusWordCount,
-  getControlButtonWriteValue,
-  getRegisterWordCache,
-  getUserStatusCount,
-  setSharedInputValues
-}

+ 0 - 281
domain/motor-control/conversions.js

@@ -1,281 +0,0 @@
-const {
-  ATT_COEF,
-  DEFAULT_DRIVER_PARAMS,
-  SCALE_MAX,
-  TWO_PI,
-  getDriverParams,
-  getSharedInputValues,
-  toFiniteNumber
-} = require('./calculation-context.js')
-const {
-  rawToTemperature,
-  temperatureToRaw
-} = require('./thermistor.js')
-
-function getSpeedBase(inputValues = {}, driverParams = DEFAULT_DRIVER_PARAMS) {
-  const candidates = [
-    inputValues['速度基准'],
-    getSharedInputValues()['速度基准'],
-    driverParams.speedBase
-  ]
-
-  for (let index = 0; index < candidates.length; index += 1) {
-    const speedBase = toFiniteNumber(candidates[index])
-
-    if (speedBase > 0) return speedBase
-  }
-
-  return 0
-}
-
-function getSampleLimits(driverParams = getDriverParams()) {
-  const baseVoltage = driverParams.baseVoltage
-  const samplingResistorOhm = driverParams.samplingResistorMohm / 1000
-  const currentSampleMax = baseVoltage / 2 / samplingResistorOhm / driverParams.opAmpGain
-  const currentBase = currentSampleMax * 2
-  const voltageSampleMax = driverParams.busVoltageDividerRatio * baseVoltage
-
-  return {
-    analogInputDividerRatio: driverParams.analogInputDividerRatio,
-    baseVoltage,
-    busVoltageDividerRatio: driverParams.busVoltageDividerRatio,
-    carrierFrequencyKHz: driverParams.carrierFrequencyKHz,
-    currentBase,
-    currentSampleMax,
-    speedBase: getSpeedBase({}, driverParams),
-    voltageSampleMax
-  }
-}
-
-function formatWriteValue(value) {
-  if (!Number.isFinite(value)) return '--'
-  return String(Math.round(value))
-}
-
-function formatFixedValue(value, precision = 2) {
-  const numberValue = toFiniteNumber(value, NaN)
-  if (!Number.isFinite(numberValue)) return '--'
-
-  const text = numberValue.toFixed(precision)
-  return Number(text) === 0 ? (0).toFixed(precision) : text
-}
-
-function calculateSpeedSlope(inputValues, driverParams = getDriverParams()) {
-  const speedMin = toFiniteNumber(inputValues['速度最小值'])
-  const speedMax = toFiniteNumber(inputValues['速度最大值'])
-  const speedBase = getSpeedBase(inputValues, driverParams)
-  const minVoltage = toFiniteNumber(inputValues['调速最低电压'])
-  const maxVoltage = toFiniteNumber(inputValues['调速最高电压'])
-  const { baseVoltage, analogInputDividerRatio } = getSampleLimits(driverParams)
-
-  if (speedMax <= speedMin || maxVoltage <= minVoltage || !speedBase) return null
-
-  return (speedMax - speedMin) / speedBase / ((maxVoltage - minVoltage) * analogInputDividerRatio / baseVoltage)
-}
-
-function calculateSpeedCommandWriteValue(actualValue, inputValues = {}, driverParams = getDriverParams()) {
-  if (actualValue === '' || actualValue === undefined || actualValue === null) return '--'
-
-  const numberValue = toFiniteNumber(actualValue, NaN)
-  const speedBase = getSpeedBase(inputValues, driverParams)
-
-  if (!Number.isFinite(numberValue) || !speedBase) return '--'
-
-  return formatWriteValue(numberValue / speedBase * SCALE_MAX)
-}
-
-function calculateAtoGainWriteValues(atoBandwidth, inputValues = {}, driverParams = getDriverParams()) {
-  if (atoBandwidth === '' || atoBandwidth === undefined || atoBandwidth === null) {
-    return {
-      kpWriteValue: '--',
-      kiWriteValue: '--'
-    }
-  }
-
-  const bandwidth = toFiniteNumber(atoBandwidth, NaN)
-  const speedBase = getSpeedBase(inputValues, driverParams)
-  const polePairs = toFiniteNumber(inputValues['极对数'])
-  const sampleFrequency = driverParams.carrierFrequencyKHz * 1000
-  const tpwmValue = 1 / sampleFrequency
-  const baseFrequency = speedBase / 60 * polePairs
-
-  if (!Number.isFinite(bandwidth) || !speedBase || !polePairs || !baseFrequency) {
-    return {
-      kpWriteValue: '--',
-      kiWriteValue: '--'
-    }
-  }
-
-  return {
-    kpWriteValue: formatWriteValue(4095 * TWO_PI * ATT_COEF * bandwidth / baseFrequency),
-    kiWriteValue: formatWriteValue(SCALE_MAX * TWO_PI * bandwidth * bandwidth * tpwmValue / baseFrequency)
-  }
-}
-
-function calculateDqGainWriteValue(item, actualValue) {
-  if (actualValue === '' || actualValue === undefined || actualValue === null) return '--'
-
-  const numberValue = toFiniteNumber(actualValue, NaN)
-  if (!Number.isFinite(numberValue)) return '--'
-
-  if (item.gainType === 'kp') return formatWriteValue(4095 * numberValue)
-  if (item.gainType === 'ki') return formatWriteValue(SCALE_MAX * numberValue)
-
-  return formatWriteValue(numberValue)
-}
-
-function getFloatPrecision(item) {
-  if (item.name === 'LD' || item.name === 'LQ') return 6
-  if (item.name === 'RS') return 4
-
-  return 2
-}
-
-function calculateParameterInputWriteValue(item, actualValue, inputValues = {}, driverParams = getDriverParams()) {
-  if (actualValue === '' || actualValue === undefined || actualValue === null) return '--'
-
-  const numberValue = toFiniteNumber(actualValue, NaN)
-  if (!Number.isFinite(numberValue)) return '--'
-
-  const { baseVoltage, analogInputDividerRatio } = getSampleLimits(driverParams)
-  const speedBase = getSpeedBase(inputValues, driverParams)
-
-  if (['开机电压', '关机电压', '调速最低电压', '调速最高电压'].includes(item.name)) {
-    return formatWriteValue(SCALE_MAX * numberValue / baseVoltage * analogInputDividerRatio)
-  }
-
-  if (['速度最小值', '速度最大值'].includes(item.name)) {
-    if (!speedBase) return '--'
-
-    return formatWriteValue(SCALE_MAX * numberValue / speedBase)
-  }
-
-  if (item.name === 'SOUT_MAX') {
-    const { currentBase } = getSampleLimits(driverParams)
-
-    return formatWriteValue(numberValue / currentBase * SCALE_MAX)
-  }
-
-  if (item.name === '上油转速') {
-    if (!speedBase) return '--'
-
-    return formatWriteValue(SCALE_MAX * numberValue / speedBase)
-  }
-
-  if (item.type === 'uint8_t') {
-    if (numberValue < 0 || numberValue > 0xFF) return '--'
-
-    return formatWriteValue(numberValue)
-  }
-
-  if (item.type === 'float') return formatFixedValue(numberValue, getFloatPrecision(item))
-
-  return formatWriteValue(numberValue)
-}
-
-function calculateProtectionWriteValue(item, actualValue, driverParams = getDriverParams()) {
-  if (!actualValue && actualValue !== 0) return '--'
-
-  const { currentBase, currentSampleMax, voltageSampleMax, speedBase } = getSampleLimits(driverParams)
-
-  if (['过压保护值', '欠压保护值', '过压恢复值', '欠压恢复值'].includes(item.name)) {
-    return formatWriteValue(actualValue / voltageSampleMax * SCALE_MAX)
-  }
-
-  if (item.name === '软件过流值') {
-    return formatWriteValue(actualValue / currentBase * SCALE_MAX)
-  }
-
-  if (['速度限制最大值', '速度限制最小值', '速度中间值'].includes(item.name)) {
-    return formatWriteValue(actualValue / speedBase * SCALE_MAX)
-  }
-
-  if (item.name === '功率保护值') {
-    return formatWriteValue(actualValue / currentSampleMax / voltageSampleMax * SCALE_MAX)
-  }
-
-  if (item.name === '温度保护值' || item.name === '温度恢复值') {
-    const rawValue = temperatureToRaw(actualValue, SCALE_MAX)
-
-    return rawValue === null ? '--' : formatWriteValue(rawValue)
-  }
-
-  return item.type === 'float' ? formatFixedValue(Number(actualValue), 2) : formatWriteValue(actualValue)
-}
-
-function calculateParameterReadValue(item, rawValue, inputValues = {}, driverParams = getDriverParams()) {
-  const rawNumber = toFiniteNumber(rawValue, NaN)
-  if (!Number.isFinite(rawNumber)) return null
-
-  const ratio = rawNumber / SCALE_MAX
-  const {
-    analogInputDividerRatio,
-    baseVoltage,
-    currentBase,
-    currentSampleMax,
-    voltageSampleMax
-  } = getSampleLimits(driverParams)
-  const speedBase = getSpeedBase(inputValues, driverParams)
-
-  if (['开机电压', '关机电压', '调速最低电压', '调速最高电压'].includes(item.name)) {
-    return analogInputDividerRatio ? ratio * baseVoltage / analogInputDividerRatio : null
-  }
-
-  if (['过压保护值', '欠压保护值', '过压恢复值', '欠压恢复值'].includes(item.name)) {
-    return ratio * voltageSampleMax
-  }
-
-  if (['速度最小值', '速度最大值', '上油转速', '速度限制最大值', '速度限制最小值', '速度中间值'].includes(item.name)) {
-    return speedBase ? ratio * speedBase : null
-  }
-
-  if (item.name === '软件过流值' || item.name === 'SOUT_MAX') {
-    return ratio * currentBase
-  }
-
-  if (item.name === '功率保护值') {
-    return ratio * currentSampleMax * voltageSampleMax
-  }
-
-  if (item.name === '温度保护值' || item.name === '温度恢复值') {
-    return rawToTemperature(rawNumber, SCALE_MAX)
-  }
-
-  return null
-}
-
-function calculateStatusValue(name, rawValue, driverParams = getDriverParams()) {
-  const rawNumber = toFiniteNumber(rawValue)
-  const ratio = rawNumber / SCALE_MAX
-  const {
-    analogInputDividerRatio,
-    baseVoltage,
-    busVoltageDividerRatio,
-    currentBase,
-    currentSampleMax,
-    voltageSampleMax,
-    speedBase
-  } = getSampleLimits(driverParams)
-
-  if (name === '母线电压') return ratio * baseVoltage * busVoltageDividerRatio
-  if (name === '母线电流') return ratio * currentBase
-  if (name === 'NTC 温度') return rawToTemperature(rawNumber, SCALE_MAX)
-  if (name === '模拟输入电压') return analogInputDividerRatio ? ratio * baseVoltage / analogInputDividerRatio : 0
-  if (name === '估算速度') return ratio * speedBase
-  if (name === '估算功率') return ratio * voltageSampleMax * currentSampleMax
-  if (name === '频率' || name === '占空比') return rawNumber / 10
-  return rawNumber
-}
-
-module.exports = {
-  SCALE_MAX,
-  calculateAtoGainWriteValues,
-  calculateDqGainWriteValue,
-  calculateParameterInputWriteValue,
-  calculateParameterReadValue,
-  calculateProtectionWriteValue,
-  calculateSpeedCommandWriteValue,
-  calculateSpeedSlope,
-  calculateStatusValue,
-  formatFixedValue
-}

+ 0 - 21
domain/motor-control/data.js

@@ -1,21 +0,0 @@
-const calculationContext = require('./calculation-context.js')
-const controlState = require('./control-state.js')
-const conversions = require('./conversions.js')
-const inputValueUtils = require('./input-value-utils.js')
-const paramsState = require('./params-state.js')
-const registerValueUtils = require('../../utils/register-value-utils.js')
-const registers = require('./registers.js')
-const statusFormat = require('./status-format.js')
-const statusState = require('./status-state.js')
-
-module.exports = {
-  calculationContext,
-  controlState,
-  conversions,
-  inputValueUtils,
-  paramsState,
-  registerValueUtils,
-  registers,
-  statusFormat,
-  statusState
-}

+ 0 - 7
domain/motor-control/index.js

@@ -1,7 +0,0 @@
-module.exports = {
-  conversions: require('./conversions.js'),
-  data: require('./data.js'),
-  registerGroups: require('./register-groups.js'),
-  registers: require('./registers.js'),
-  statusFormat: require('./status-format.js')
-}

+ 0 - 29
domain/motor-control/input-value-utils.js

@@ -1,29 +0,0 @@
-const {
-  toFiniteNumber
-} = require('./calculation-context.js')
-
-function getInputTextWithoutUnit(item, value) {
-  const text = String(value === undefined || value === null ? '' : value).trim()
-  if (!item || !item.unit || !text) return text
-
-  const lowerText = text.toLowerCase()
-  const lowerUnit = String(item.unit).toLowerCase()
-
-  return lowerText.endsWith(lowerUnit)
-    ? text.slice(0, -item.unit.length).trim()
-    : text
-}
-
-function appendInputUnit(item, value) {
-  const text = String(value === undefined || value === null ? '' : value).trim()
-  if (!item || !item.unit || !text) return text
-
-  const valueText = getInputTextWithoutUnit(item, text)
-  if (!Number.isFinite(toFiniteNumber(valueText, NaN))) return text
-
-  return `${valueText} ${item.unit}`
-}
-
-module.exports = {
-  appendInputUnit
-}

+ 0 - 890
domain/motor-control/params-state.js

@@ -1,890 +0,0 @@
-const {
-  atoBandwidthInputRegisters,
-  calculatedParameterRegisters,
-  dqGainInputRegisters,
-  oilParameterInputRegisters,
-  parameterInputRegisters,
-  prepositionParameterInputRegisters,
-  protectionRegisters,
-  protectionSwitchRegisters,
-  speedLoopExtraRegisters,
-  speedSlopeRegister,
-  tailwindSwitchRegisters,
-  getByteRegisterValue
-} = require('./registers.js')
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const {
-  getSharedInputValues,
-  mergeInputValues,
-  toFiniteNumber
-} = require('./calculation-context.js')
-const {
-  calculateAtoGainWriteValues,
-  calculateDqGainWriteValue,
-  calculateParameterInputWriteValue,
-  calculateParameterReadValue,
-  calculateProtectionWriteValue,
-  calculateSpeedSlope,
-  formatFixedValue
-} = require('./conversions.js')
-const {
-  toAddressKey,
-  wordsToFloat
-} = require('../../utils/register-value-utils.js')
-const {
-  appendInputUnit
-} = require('./input-value-utils.js')
-
-const VSP_CURVE_ORDER = [
-  '开机电压',
-  '关机电压',
-  '调速最低电压',
-  '调速最高电压'
-]
-
-const SPEED_LOOP_INPUT_ORDER = [
-  '速度最小值',
-  '速度最大值',
-  'SOUT_MAX'
-]
-
-const TAILWIND_CALCULATED_NAMES = [
-  'SPEED_KLPF_TAILWIND',
-  'OBS_EA_KS_TAILWIND'
-]
-
-const ESTIMATOR_CALCULATED_PREFIXES = [
-  'OBS_E',
-  'OBS_FBASE',
-  'OBS_EA_KS',
-  'SPEED_KLPF',
-  'FOC_KFG'
-]
-
-const PROTECTION_SECTION_DEFINITIONS = [
-  {
-    key: 'base',
-    title: '基础',
-    rows: [
-      [
-        { kind: 'switch', name: '保护使能' },
-        { kind: 'switch', name: '恢复使能' },
-        { kind: 'input', name: '故障恢复时间' }
-      ]
-    ]
-  },
-  {
-    key: 'hardwareCurrent',
-    title: '硬件过流',
-    rows: [
-      [
-        { kind: 'input', name: '硬件过流值', label: '硬件过流' }
-      ]
-    ]
-  },
-  {
-    key: 'current',
-    title: '电流',
-    rows: [
-      [
-        { kind: 'switch', name: '电流保护使能' },
-        { kind: 'input', name: '软件过流值' }
-      ]
-    ]
-  },
-  {
-    key: 'phase',
-    title: '缺相',
-    rows: [
-      [
-        { kind: 'switch', name: '缺相保护使能' }
-      ]
-    ]
-  },
-  {
-    key: 'voltage',
-    title: '电压',
-    rows: [
-      [
-        { kind: 'switch', name: '电压保护使能' }
-      ],
-      [
-        { kind: 'input', name: '过压保护值', label: '过压值' },
-        { kind: 'input', name: '欠压保护值', label: '欠压值' }
-      ],
-      [
-        { kind: 'input', name: '过压恢复值' },
-        { kind: 'input', name: '欠压恢复值' }
-      ]
-    ]
-  },
-  {
-    key: 'stall',
-    title: '堵转',
-    rows: [
-      [
-        { kind: 'switch', name: '堵转保护使能' }
-      ],
-      [
-        { kind: 'input', name: '速度限制最大值' },
-        { kind: 'input', name: '速度限制最小值' }
-      ],
-      [
-        { kind: 'input', name: '反电动势低阈值' },
-        { kind: 'input', name: '反电动势高阈值' }
-      ],
-      [
-        { kind: 'input', name: '速度中间值' }
-      ]
-    ]
-  },
-  {
-    key: 'power',
-    title: '功率',
-    rows: [
-      [
-        { kind: 'switch', name: '功率保护使能' },
-        { kind: 'input', name: '功率保护值' },
-        { kind: 'input', name: '功率保护时间' }
-      ]
-    ]
-  },
-  {
-    key: 'temperature',
-    title: '温度',
-    rows: [
-      [
-        { kind: 'switch', name: '温度保护使能' }
-      ],
-      [
-        { kind: 'input', name: '温度保护值' },
-        { kind: 'input', name: '温度恢复值' },
-        { kind: 'input', name: '温度保护时间' }
-      ]
-    ]
-  },
-  {
-    key: 'serial',
-    title: '串口',
-    rows: [
-      [
-        { kind: 'switch', name: '串口保护使能' },
-        { kind: 'input', name: '串口丢失检测时间' }
-      ]
-    ]
-  },
-  {
-    key: 'pwm',
-    title: 'PWM',
-    rows: [
-      [
-        { kind: 'switch', name: 'PWM丢失保护使能' }
-      ]
-    ]
-  }
-]
-
-function formatInputValue(item, value) {
-  if (value === '' || value === undefined || value === null) return '--'
-
-  const numberValue = toFiniteNumber(value, NaN)
-  if (!Number.isFinite(numberValue)) return value
-
-  if (item.type === 'uint8_t' && (numberValue < 0 || numberValue > 0xFF)) return '--'
-
-  if (item.type === 'float') return formatFixedValue(numberValue, 2)
-
-  return String(Math.round(numberValue))
-}
-
-function getInputValues(registers) {
-  return mergeInputValues(registers)
-}
-
-function getSharedParameterValues(registers, extraRegisters = []) {
-  return {
-    ...getSharedInputValues(),
-    ...getInputValues(registers.concat(extraRegisters))
-  }
-}
-
-function updateAtoBandwidthValues(registers, inputValues) {
-  return registers.map((item) => ({
-    ...item,
-    ...calculateAtoGainWriteValues(item.inputValue, inputValues)
-  }))
-}
-
-function updateInputWriteValues(registers) {
-  const inputValues = getInputValues(registers)
-
-  return registers.map((item) => ({
-    ...item,
-    writeValue: calculateParameterInputWriteValue(item, item.inputValue, inputValues)
-  }))
-}
-
-function updateSpeedLoopExtraValues(registers, inputValues) {
-  return registers.map((item) => {
-    const writeValue = calculateParameterInputWriteValue(item, item.inputValue, inputValues)
-
-    return {
-      ...item,
-      actualText: '',
-      writeValue
-    }
-  })
-}
-
-function updateOilParameterValues(registers, inputValues) {
-  return registers.map((item) => ({
-    ...item,
-    writeValue: calculateParameterInputWriteValue(item, item.inputValue, inputValues)
-  }))
-}
-
-function updateSpeedSlope(register, inputValues) {
-  const speedSlope = calculateSpeedSlope(inputValues)
-
-  return {
-    ...register,
-    writeValue: speedSlope === null ? '--' : formatFixedValue(speedSlope, 2)
-  }
-}
-
-function addSourceIndex(registers) {
-  return registers.map((item, index) => ({
-    ...item,
-    sourceIndex: index
-  }))
-}
-
-function isNameIn(names, item) {
-  return names.includes(item.name)
-}
-
-function isTailwindAtoRegister(item) {
-  return item.suffix === 'TAILWIND'
-}
-
-function isTailwindCalculatedRegister(item) {
-  return isNameIn(TAILWIND_CALCULATED_NAMES, item)
-}
-
-function isEstimatorCalculatedRegister(item) {
-  if (isTailwindCalculatedRegister(item)) return false
-
-  return ESTIMATOR_CALCULATED_PREFIXES.some((prefix) => item.name.startsWith(prefix))
-}
-
-function sortByNameOrder(registers, nameOrder) {
-  return registers
-    .filter((item) => nameOrder.includes(item.name))
-    .slice()
-    .sort((left, right) => nameOrder.indexOf(left.name) - nameOrder.indexOf(right.name))
-}
-
-function mapByName(registers) {
-  return registers.reduce((result, item) => {
-    result[item.name] = item
-    return result
-  }, {})
-}
-
-function buildProtectionField(definition, registerMap, switchMap) {
-  const source = definition.kind === 'switch'
-    ? switchMap[definition.name]
-    : registerMap[definition.name]
-
-  if (!source) return null
-
-  return {
-    ...source,
-    kind: definition.kind,
-    label: definition.label || source.name,
-    metaValue: source.writeValue === 0 ? '0' : (source.writeValue || '--')
-  }
-}
-
-function buildProtectionGroups(registers, switches = []) {
-  const protectionDisplayRegisters = addSourceIndex(registers)
-  const protectionSwitchDisplayRegisters = addSourceIndex(switches)
-  const registerMap = mapByName(protectionDisplayRegisters)
-  const switchMap = mapByName(protectionSwitchDisplayRegisters)
-
-  return {
-    protectionDisplayRegisters,
-    protectionSections: PROTECTION_SECTION_DEFINITIONS.map((section) => ({
-      ...section,
-      rows: section.rows
-        .map((row, rowIndex) => {
-          const fields = row
-            .map((definition) => buildProtectionField(definition, registerMap, switchMap))
-            .filter(Boolean)
-
-          return fields.length
-            ? {
-              fields,
-              key: `${section.key}-${rowIndex}`
-            }
-            : null
-        })
-        .filter(Boolean)
-    })).filter((section) => section.rows.length)
-  }
-}
-
-function getWord(readValues, address) {
-  return readValues.words[toAddressKey(address)]
-}
-
-function getRegisterReadValue(item, readValues) {
-  if (item.area && item.area.key === 'coil') {
-    const coilValue = readValues.coils[toAddressKey(item.address)]
-    return coilValue === undefined ? null : coilValue
-  }
-
-  const firstWord = getWord(readValues, item.address)
-  if (!Number.isInteger(firstWord)) return null
-
-  if (item.type === 'uint8_t' && item.bytePosition) {
-    return getByteRegisterValue(item, firstWord)
-  }
-
-  if (item.type === 'float') {
-    const nextAddress = (parseHexInteger(item.address) + 1).toString(16).toUpperCase()
-    return wordsToFloat(firstWord, getWord(readValues, nextAddress))
-  }
-
-  return firstWord
-}
-
-function formatReadValue(item, value) {
-  if (value === null || value === undefined || !Number.isFinite(Number(value))) return null
-
-  const numberValue = Number(value)
-
-  if (item.type === 'float') return formatFixedValue(numberValue, 2)
-
-  return String(Math.round(numberValue))
-}
-
-function getReadInputValue(item, readValue, readText, options = {}) {
-  let inputText = readText
-
-  if (options.useCalculatedInputValue) {
-    const calculatedValue = calculateParameterReadValue(item, readValue, options.inputValues || {})
-
-    if (calculatedValue !== null) {
-      inputText = formatFixedValue(calculatedValue, 2)
-    }
-  }
-
-  return appendInputUnit(item, inputText)
-}
-
-function getDqReadInputValue(item, rawText) {
-  const rawValue = Number(rawText)
-  if (!Number.isFinite(rawValue)) return ''
-
-  if (item.gainType === 'kp') return formatFixedValue(rawValue / 4095, 2)
-  if (item.gainType === 'ki') return formatFixedValue(rawValue / 32767, 2)
-
-  return rawText
-}
-
-function applyReadValuesToRegisters(registers, readValues, options = {}) {
-  return registers.map((item) => {
-    const readValue = getRegisterReadValue(item, readValues)
-    const readText = formatReadValue(item, readValue)
-
-    if (readText === null) return item
-
-    const nextItem = {
-      ...item,
-      isDirty: false,
-      writeValue: readText
-    }
-
-    if (Object.prototype.hasOwnProperty.call(item, 'value')) {
-      nextItem.value = Number(readValue) !== 0
-    }
-
-    if (options.updateInputValue) {
-      nextItem.inputValue = getReadInputValue(item, readValue, readText, options)
-    }
-
-    if (options.updateDqInputValue) {
-      nextItem.inputValue = getDqReadInputValue(item, readText)
-    }
-
-    return nextItem
-  })
-}
-
-function applyReadValuesToAtoRegisters(registers, readValues) {
-  return registers.map((item) => {
-    const kpWord = getWord(readValues, item.kpAddress)
-    const kiWord = getWord(readValues, item.kiAddress)
-
-    if (!Number.isInteger(kpWord) && !Number.isInteger(kiWord)) return item
-
-    return {
-      ...item,
-      isDirty: false,
-      kpWriteValue: Number.isInteger(kpWord) ? String(kpWord) : item.kpWriteValue,
-      kiWriteValue: Number.isInteger(kiWord) ? String(kiWord) : item.kiWriteValue
-    }
-  })
-}
-
-function clearDirty(registers = [], matcher = () => true) {
-  return registers.map((item) => (
-    matcher(item)
-      ? {
-        ...item,
-        isDirty: false
-      }
-      : item
-  ))
-}
-
-function clearGroupDirty(data, groupKey) {
-  const nextState = {
-    ...data
-  }
-
-  if (groupKey === 'estimator') {
-    nextState.atoBandwidthInputRegisters = clearDirty(data.atoBandwidthInputRegisters)
-    nextState.calculatedParameterRegisters = clearDirty(data.calculatedParameterRegisters, isEstimatorCalculatedRegister)
-  }
-  if (groupKey === 'dq') nextState.dqGainInputRegisters = clearDirty(data.dqGainInputRegisters)
-  if (groupKey === 'tailwind') {
-    nextState.tailwindSwitchRegisters = clearDirty(data.tailwindSwitchRegisters, (item) => item.name !== '预定位启用')
-    nextState.atoBandwidthInputRegisters = clearDirty(data.atoBandwidthInputRegisters, isTailwindAtoRegister)
-    nextState.calculatedParameterRegisters = clearDirty(data.calculatedParameterRegisters, isTailwindCalculatedRegister)
-  }
-  if (groupKey === 'preposition') {
-    nextState.tailwindSwitchRegisters = clearDirty(data.tailwindSwitchRegisters, (item) => item.name === '预定位启用')
-    nextState.prepositionParameterInputRegisters = clearDirty(data.prepositionParameterInputRegisters)
-  }
-  if (groupKey === 'speedLoop') {
-    nextState.parameterInputRegisters = clearDirty(data.parameterInputRegisters, (item) => (
-      SPEED_LOOP_INPUT_ORDER.includes(item.name)
-    ))
-    nextState.speedLoopExtraRegisters = clearDirty(data.speedLoopExtraRegisters)
-  }
-  if (groupKey === 'vsp') {
-    nextState.parameterInputRegisters = clearDirty(data.parameterInputRegisters, (item) => VSP_CURVE_ORDER.includes(item.name))
-    nextState.speedSlopeRegister = {
-      ...data.speedSlopeRegister,
-      isDirty: false
-    }
-  }
-  if (groupKey === 'oil') nextState.oilParameterInputRegisters = clearDirty(data.oilParameterInputRegisters)
-  if (groupKey === 'protection') {
-    nextState.protectionRegisters = clearDirty(data.protectionRegisters)
-    nextState.protectionSwitchRegisters = clearDirty(data.protectionSwitchRegisters)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function clearRegisterDirty(registers = [], index) {
-  return registers.map((item, currentIndex) => (
-    currentIndex === index
-      ? {
-        ...item,
-        isDirty: false
-      }
-      : item
-  ))
-}
-
-function clearTailwindSwitchDirty(data, index) {
-  const nextState = {
-    ...data,
-    tailwindSwitchRegisters: clearRegisterDirty(data.tailwindSwitchRegisters, index)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function clearProtectionSwitchDirty(data, index) {
-  const nextState = {
-    ...data,
-    protectionSwitchRegisters: clearRegisterDirty(data.protectionSwitchRegisters, index)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function buildViewState(state) {
-  const inputRegisters = addSourceIndex(state.parameterInputRegisters)
-  const atoRegisters = addSourceIndex(state.atoBandwidthInputRegisters)
-  const dqRegisters = addSourceIndex(state.dqGainInputRegisters)
-  const calculatedRegisters = addSourceIndex(state.calculatedParameterRegisters)
-  const speedLoopExtras = addSourceIndex(state.speedLoopExtraRegisters)
-  const tailwindSwitches = addSourceIndex(state.tailwindSwitchRegisters)
-
-  return {
-    vspCurveRegisters: sortByNameOrder(inputRegisters, VSP_CURVE_ORDER),
-    speedLoopInputDisplayRegisters: sortByNameOrder(inputRegisters, SPEED_LOOP_INPUT_ORDER),
-    speedLoopExtraDisplayRegisters: speedLoopExtras,
-    atoBandwidthDisplayRegisters: atoRegisters.filter((item) => !isTailwindAtoRegister(item)),
-    tailwindAtoBandwidthDisplayRegisters: atoRegisters.filter(isTailwindAtoRegister),
-    tailwindCalculatedDisplayRegisters: calculatedRegisters.filter(isTailwindCalculatedRegister),
-    tailwindControlRegisters: tailwindSwitches.filter((item) => item.name !== '预定位启用'),
-    prepositionSwitchRegisters: tailwindSwitches.filter((item) => item.name === '预定位启用'),
-    prepositionParameterDisplayRegisters: addSourceIndex(state.prepositionParameterInputRegisters),
-    dqGainDisplayRegisters: dqRegisters,
-    estimatorCalculatedDisplayRegisters: calculatedRegisters.filter(isEstimatorCalculatedRegister),
-    ...buildProtectionGroups(state.protectionRegisters, state.protectionSwitchRegisters)
-  }
-}
-
-function createInitialState() {
-  const state = {
-    atoBandwidthInputRegisters,
-    atoBandwidthDisplayRegisters: [],
-    calculatedParameterRegisters,
-    dqGainInputRegisters,
-    dqGainDisplayRegisters: [],
-    estimatorCalculatedDisplayRegisters: [],
-    oilParameterInputRegisters,
-    parameterInputRegisters,
-    prepositionParameterDisplayRegisters: [],
-    prepositionParameterInputRegisters,
-    prepositionSwitchRegisters: [],
-    protectionDisplayRegisters: [],
-    protectionSections: [],
-    protectionRegisters,
-    protectionSwitchRegisters,
-    speedLoopExtraRegisters,
-    speedLoopExtraDisplayRegisters: [],
-    speedLoopInputDisplayRegisters: [],
-    speedSlopeRegister,
-    tailwindAtoBandwidthDisplayRegisters: [],
-    tailwindCalculatedDisplayRegisters: [],
-    tailwindControlRegisters: [],
-    tailwindSwitchRegisters,
-    vspCurveRegisters: []
-  }
-
-  return {
-    ...state,
-    ...buildViewState(state)
-  }
-}
-
-function refreshState(data) {
-  const inputValues = getSharedParameterValues(data.parameterInputRegisters, data.speedLoopExtraRegisters)
-  const nextSpeedLoopExtraRegisters = updateSpeedLoopExtraValues(data.speedLoopExtraRegisters, inputValues)
-  const nextState = {
-    ...data,
-    atoBandwidthInputRegisters: updateAtoBandwidthValues(data.atoBandwidthInputRegisters, inputValues),
-    oilParameterInputRegisters: updateOilParameterValues(data.oilParameterInputRegisters, inputValues),
-    speedLoopExtraRegisters: nextSpeedLoopExtraRegisters,
-    speedSlopeRegister: updateSpeedSlope(data.speedSlopeRegister, inputValues)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyReadValues(data, readValues) {
-  const inputValues = getSharedParameterValues(data.parameterInputRegisters, data.speedLoopExtraRegisters)
-  const nextState = {
-    ...data,
-    atoBandwidthInputRegisters: applyReadValuesToAtoRegisters(data.atoBandwidthInputRegisters, readValues),
-    calculatedParameterRegisters: applyReadValuesToRegisters(data.calculatedParameterRegisters, readValues),
-    dqGainInputRegisters: applyReadValuesToRegisters(data.dqGainInputRegisters, readValues, {
-      updateDqInputValue: true
-    }),
-    oilParameterInputRegisters: applyReadValuesToRegisters(data.oilParameterInputRegisters, readValues, {
-      inputValues,
-      updateInputValue: true,
-      useCalculatedInputValue: true
-    }),
-    parameterInputRegisters: applyReadValuesToRegisters(data.parameterInputRegisters, readValues, {
-      inputValues,
-      updateInputValue: true,
-      useCalculatedInputValue: true
-    }),
-    prepositionParameterInputRegisters: applyReadValuesToRegisters(data.prepositionParameterInputRegisters, readValues, {
-      updateInputValue: true
-    }),
-    protectionRegisters: applyReadValuesToRegisters(data.protectionRegisters, readValues, {
-      inputValues,
-      updateInputValue: true,
-      useCalculatedInputValue: true
-    }),
-    protectionSwitchRegisters: applyReadValuesToRegisters(data.protectionSwitchRegisters, readValues),
-    speedLoopExtraRegisters: applyReadValuesToRegisters(data.speedLoopExtraRegisters, readValues, {
-      updateInputValue: true
-    }),
-    speedSlopeRegister: applyReadValuesToRegisters([data.speedSlopeRegister], readValues)[0],
-    tailwindSwitchRegisters: applyReadValuesToRegisters(data.tailwindSwitchRegisters, readValues)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyParameterInput(data, index, value) {
-  const changedRegisters = data.parameterInputRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value,
-      writeValue: formatInputValue(item, value)
-    }
-  })
-  const nextRegisters = updateInputWriteValues(changedRegisters)
-  const inputValues = getSharedParameterValues(nextRegisters, data.speedLoopExtraRegisters)
-  const nextState = {
-    ...data,
-    parameterInputRegisters: nextRegisters,
-    atoBandwidthInputRegisters: updateAtoBandwidthValues(data.atoBandwidthInputRegisters, inputValues),
-    speedLoopExtraRegisters: updateSpeedLoopExtraValues(data.speedLoopExtraRegisters, inputValues),
-    speedSlopeRegister: updateSpeedSlope(data.speedSlopeRegister, inputValues)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyAtoBandwidthInput(data, index, value) {
-  const inputValues = getSharedParameterValues(data.parameterInputRegisters)
-  const nextRegisters = data.atoBandwidthInputRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value,
-      ...calculateAtoGainWriteValues(value, inputValues)
-    }
-  })
-  const nextState = {
-    ...data,
-    atoBandwidthInputRegisters: nextRegisters
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyDqGainInput(data, index, value) {
-  const nextRegisters = data.dqGainInputRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value,
-      writeValue: calculateDqGainWriteValue(item, value)
-    }
-  })
-  const nextState = {
-    ...data,
-    dqGainInputRegisters: nextRegisters
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applySpeedLoopExtraInput(data, index, value) {
-  const changedRegisters = data.speedLoopExtraRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value
-    }
-  })
-  const inputValues = getSharedParameterValues(data.parameterInputRegisters, changedRegisters)
-  const nextState = {
-    ...data,
-    speedLoopExtraRegisters: updateSpeedLoopExtraValues(changedRegisters, inputValues)
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyOilParameterInput(data, index, value) {
-  const changedRegisters = data.oilParameterInputRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value
-    }
-  })
-  const inputValues = getSharedParameterValues(data.parameterInputRegisters, data.speedLoopExtraRegisters)
-  const nextState = {
-    ...data,
-    oilParameterInputRegisters: updateOilParameterValues(changedRegisters, inputValues)
-  }
-
-  return nextState
-}
-
-function applyPrepositionParameterInput(data, index, value) {
-  const nextState = {
-    ...data,
-    prepositionParameterInputRegisters: data.prepositionParameterInputRegisters.map((item, currentIndex) => {
-      if (currentIndex !== index) return item
-
-      return {
-        ...item,
-        isDirty: true,
-        inputValue: value,
-        writeValue: formatInputValue(item, value)
-      }
-    })
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyTailwindSwitchChange(data, index, checked) {
-  const nextState = {
-    ...data,
-    tailwindSwitchRegisters: data.tailwindSwitchRegisters.map((item, currentIndex) => {
-      if (currentIndex !== index) return item
-
-      return {
-        ...item,
-        isDirty: true,
-        value: checked,
-        writeValue: checked ? 1 : 0
-      }
-    })
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyProtectionSwitchChange(data, index, checked) {
-  const nextState = {
-    ...data,
-    protectionSwitchRegisters: data.protectionSwitchRegisters.map((item, currentIndex) => {
-      if (currentIndex !== index) return item
-
-      return {
-        ...item,
-        isDirty: true,
-        value: checked,
-        writeValue: checked ? 1 : 0
-      }
-    })
-  }
-
-  return {
-    ...nextState,
-    ...buildViewState(nextState)
-  }
-}
-
-function applyProtectionInput(data, index, value) {
-  const nextRegisters = data.protectionRegisters.map((item, currentIndex) => {
-    if (currentIndex !== index) return item
-
-    return {
-      ...item,
-      isDirty: true,
-      inputValue: value,
-      writeValue: value === '' ? '--' : calculateProtectionWriteValue(item, toFiniteNumber(value, NaN))
-    }
-  })
-
-  return {
-    ...data,
-    protectionRegisters: nextRegisters,
-    ...buildProtectionGroups(nextRegisters, data.protectionSwitchRegisters)
-  }
-}
-
-function getInputRegister(data, group, index) {
-  if (group === 'parameter') return data.parameterInputRegisters[index]
-  if (group === 'ato') return data.atoBandwidthInputRegisters[index]
-  if (group === 'dq') return data.dqGainInputRegisters[index]
-  if (group === 'speedLoopExtra') return data.speedLoopExtraRegisters[index]
-  if (group === 'oil') return data.oilParameterInputRegisters[index]
-  if (group === 'preposition') return data.prepositionParameterInputRegisters[index]
-  if (group === 'protection') return data.protectionRegisters[index]
-
-  return null
-}
-
-function applyInputBlur(data, group, index, value) {
-  const item = getInputRegister(data, group, index)
-  if (!item) return data
-
-  const inputValue = appendInputUnit(item, value === undefined ? item.inputValue : value)
-
-  if (group === 'parameter') return applyParameterInput(data, index, inputValue)
-  if (group === 'ato') return applyAtoBandwidthInput(data, index, inputValue)
-  if (group === 'dq') return applyDqGainInput(data, index, inputValue)
-  if (group === 'speedLoopExtra') return applySpeedLoopExtraInput(data, index, inputValue)
-  if (group === 'oil') return applyOilParameterInput(data, index, inputValue)
-  if (group === 'preposition') return applyPrepositionParameterInput(data, index, inputValue)
-  if (group === 'protection') return applyProtectionInput(data, index, inputValue)
-
-  return data
-}
-
-module.exports = {
-  applyAtoBandwidthInput,
-  clearGroupDirty,
-  clearProtectionSwitchDirty,
-  clearTailwindSwitchDirty,
-  applyDqGainInput,
-  applyInputBlur,
-  applyOilParameterInput,
-  applyParameterInput,
-  applyPrepositionParameterInput,
-  applyProtectionInput,
-  applyProtectionSwitchChange,
-  applyReadValues,
-  applySpeedLoopExtraInput,
-  applyTailwindSwitchChange,
-  createInitialState,
-  refreshState
-}

+ 0 - 104
domain/motor-control/register-groups.js

@@ -1,104 +0,0 @@
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const {
-  getRegisterCount
-} = require('./registers.js')
-
-function parseRegisterAddress(address) {
-  return parseHexInteger(address)
-}
-
-function getAreaKey(item) {
-  return (item.area && item.area.key) || item.areaKey || 'holding'
-}
-
-function getGroupItems(data, groupKey) {
-  if (groupKey === 'vsp') return data.vspCurveRegisters.concat([data.speedSlopeRegister])
-  if (groupKey === 'speedLoop') {
-    return data.speedLoopInputDisplayRegisters
-      .concat(data.speedLoopExtraDisplayRegisters)
-  }
-  if (groupKey === 'estimator') {
-    return data.estimatorCalculatedDisplayRegisters.concat(data.atoBandwidthDisplayRegisters)
-  }
-  if (groupKey === 'tailwind') {
-    return data.tailwindControlRegisters
-      .concat(data.tailwindCalculatedDisplayRegisters, data.tailwindAtoBandwidthDisplayRegisters)
-  }
-  if (groupKey === 'preposition') return data.prepositionSwitchRegisters.concat(data.prepositionParameterDisplayRegisters)
-  if (groupKey === 'oil') return data.oilParameterInputRegisters
-  if (groupKey === 'dq') return data.dqGainDisplayRegisters
-  if (groupKey === 'protection') return data.protectionSwitchRegisters.concat(data.protectionDisplayRegisters)
-
-  return []
-}
-
-function expandAtoItems(item) {
-  if (!item.kpAddress || !item.kiAddress) return [item]
-
-  return [
-    {
-      address: item.kpAddress,
-      areaKey: 'holding',
-      name: `${item.name} KP`,
-      type: 'uint16_t',
-      writeValue: item.kpWriteValue
-    },
-    {
-      address: item.kiAddress,
-      areaKey: 'holding',
-      name: `${item.name} KI`,
-      type: 'uint16_t',
-      writeValue: item.kiWriteValue
-    }
-  ]
-}
-
-function expandItems(items) {
-  return items.reduce((result, item) => result.concat(expandAtoItems(item)), [])
-}
-
-function makeReadSpans(entries) {
-  const sortedEntries = entries
-    .map((item) => ({
-      address: parseRegisterAddress(item.address),
-      count: getRegisterCount(item)
-    }))
-    .filter((item) => Number.isFinite(item.address) && item.count > 0)
-    .sort((left, right) => left.address - right.address)
-  const spans = []
-
-  sortedEntries.forEach((entry) => {
-    const last = spans[spans.length - 1]
-
-    if (last && entry.address <= last.address + last.quantity) {
-      const end = Math.max(last.address + last.quantity, entry.address + entry.count)
-      last.quantity = end - last.address
-      return
-    }
-
-    spans.push({
-      address: entry.address,
-      quantity: entry.count
-    })
-  })
-
-  return spans
-}
-
-function mergeReadValues(target, source) {
-  if (!target || !source) return
-
-  Object.assign(target.coils, source.coils || {})
-  Object.assign(target.words, source.words || {})
-}
-
-module.exports = {
-  expandItems,
-  getAreaKey,
-  getGroupItems,
-  makeReadSpans,
-  mergeReadValues,
-  parseRegisterAddress
-}

+ 0 - 445
domain/motor-control/registers.js

@@ -1,445 +0,0 @@
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-
-const MODBUS_AREAS = {
-  coil: {
-    key: 'coil',
-    label: '线圈',
-    readFunction: '01',
-    writeFunction: '05 / 0F',
-    access: '读写'
-  },
-  input: {
-    key: 'input',
-    label: '输入寄存器',
-    readFunction: '04',
-    writeFunction: '--',
-    access: '只读'
-  },
-  holding: {
-    key: 'holding',
-    label: '保持寄存器',
-    readFunction: '03',
-    writeFunction: '06 / 10',
-    access: '读写'
-  }
-}
-
-const BYTE_POSITIONS = {
-  high: {
-    label: '高8位',
-    shift: 8
-  },
-  low: {
-    label: '低8位',
-    shift: 0
-  }
-}
-
-function hex(address) {
-  return `0x${address}`
-}
-
-function getRegisterCount(item) {
-  return item.registerCount || (item.type === 'float' ? 2 : 1)
-}
-
-function getAddressDisplay(address, registerCount) {
-  if (registerCount <= 1) return hex(address)
-
-  const start = parseHexInteger(address)
-  const end = start + registerCount - 1
-
-  return `${hex(address)}-${hex(end.toString(16).toUpperCase())}`
-}
-
-function withArea(items, areaKey, category) {
-  return items.map((item) => ({
-    ...item,
-    ...(() => {
-      const byteMeta = item.bytePosition ? BYTE_POSITIONS[item.bytePosition] : null
-      const byteLabel = item.byteLabel || (byteMeta ? byteMeta.label : '')
-      const registerCount = getRegisterCount(item)
-      const addressDisplay = byteLabel
-        ? `${getAddressDisplay(item.address, registerCount)} ${byteLabel}`
-        : getAddressDisplay(item.address, registerCount)
-
-      return {
-        byteLabel,
-        byteShift: byteMeta ? byteMeta.shift : null,
-        category,
-        addressText: hex(item.address),
-        addressDisplay,
-        registerCount,
-        area: MODBUS_AREAS[areaKey]
-      }
-    })()
-  }))
-}
-
-const OBS_ADDRESSES = {
-  EK1: '30',
-  EK2: '31',
-  EK3: '32',
-  EK4: '33',
-  FOC_KFG: '34',
-  SPEED_KLPF: '35',
-  OBS_FBASE: '36',
-  OBS_EA_KS: '37',
-  OBS_KP_START: '38',
-  OBS_KI_START: '39',
-  OBS_KP_RUN1: '3A',
-  OBS_KI_RUN1: '3B',
-  OBS_KP_RUN2: '3C',
-  OBS_KI_RUN2: '3D',
-  OBS_KP_RUN3: '3E',
-  OBS_KI_RUN3: '3F',
-  OBS_KP_RUN4: '40',
-  OBS_KI_RUN4: '41',
-  DQ_KP_START: '42',
-  DQ_KI_START: '43',
-  DQ_KP_RUN: '44',
-  DQ_KI_RUN: '45',
-  TAILWIND_SPEED_KLPF: '46',
-  TAILWIND_OBS_EA_KS: '47',
-  TAILWIND_OBS_KP: '48',
-  TAILWIND_OBS_KI: '49',
-  ALIGN_ANGLE: '4A'
-}
-
-const CONFIG_ADDRESSES = {
-  LD: '60',
-  LQ: '62',
-  RS: '64',
-  POLE_PAIRS: '66',
-  SPEED_BASE: '67',
-  SPEED_CTRL: '68',
-  CURVE_MAX_SPEED: '69',
-  CURVE_MIN_SPEED: '6A',
-  SOUT_MAX: '6B',
-  START_RAMP_INC: '6C',
-  START_RAMP_DEC: '6E',
-  RUN_RAMP_INC: '70',
-  RUN_RAMP_DEC: '72',
-  START_VOLT: '74',
-  STOP_VOLT: '75',
-  CURVE_MAX_VOLT: '76',
-  CURVE_MIN_VOLT: '77',
-  SPEED_CURVE_K: '78',
-  OIL_SPEED: '7A',
-  OIL_TIME: '7B'
-}
-
-const controlButtonRegisters = withArea([
-  { key: 'protocol', address: '00', type: 'uint8_t', name: 'Modbus控制', writeValue: 1, nextName: 'VSP控制', nextWriteValue: 0 },
-  { key: 'direction', address: '02', type: 'uint8_t', name: '正转', writeValue: 0, nextName: '反转', nextWriteValue: 1 },
-  { key: 'power', address: '01', type: 'uint8_t', name: '开机', writeValue: 1, nextName: '关机', nextWriteValue: 0 },
-  { key: 'save', address: '03', type: 'uint8_t', name: '固化', writeValue: 1, momentary: true },
-  { key: 'reset', address: '04', type: 'uint8_t', name: '复位', writeValue: 1, momentary: true }
-], 'coil', '控制类寄存器')
-
-const speedCommandRegister = withArea([
-  { address: CONFIG_ADDRESSES.SPEED_CTRL, type: 'uint16_t', name: '转速命令', unit: 'RPM', inputValue: '', writeValue: '--' }
-], 'holding', '参数配置')[0]
-
-const tailwindSwitchRegisters = withArea([
-  { address: '05', type: 'uint8_t', name: '顺逆风启用', value: false, writeValue: 0 },
-  { address: '06', type: 'uint8_t', name: '预定位启用', value: false, writeValue: 0 }
-], 'coil', '顺逆风控制')
-
-const protectionSwitchRegisters = withArea([
-  { address: '07', type: 'uint8_t', name: '保护使能', value: false, writeValue: 0 },
-  { address: '08', type: 'uint8_t', name: '恢复使能', value: false, writeValue: 0 },
-  { address: '09', type: 'uint8_t', name: '电压保护使能', value: false, writeValue: 0 },
-  { address: '0A', type: 'uint8_t', name: '电流保护使能', value: false, writeValue: 0 },
-  { address: '0B', type: 'uint8_t', name: '堵转保护使能', value: false, writeValue: 0 },
-  { address: '0C', type: 'uint8_t', name: '功率保护使能', value: false, writeValue: 0 },
-  { address: '0D', type: 'uint8_t', name: '温度保护使能', value: false, writeValue: 0 },
-  { address: '0E', type: 'uint8_t', name: '缺相保护使能', value: false, writeValue: 0 },
-  { address: '0F', type: 'uint8_t', name: 'PWM丢失保护使能', value: false, writeValue: 0 },
-  { address: '10', type: 'uint8_t', name: '串口保护使能', value: false, writeValue: 0 }
-], 'coil', '保护')
-
-const estimatorRegisters = withArea([
-  { address: OBS_ADDRESSES.EK1, type: 'uint16_t', name: 'OBS_E1K', conversion: '2047 * 3.0 / 125.0 * LQ / TPWM_VALUE * 电流基准 / 电压基准' },
-  { address: OBS_ADDRESSES.EK2, type: 'uint16_t', name: 'OBS_E2K', conversion: '2047 * 0.8 * RS * 电流基准 / 电压基准' },
-  { address: OBS_ADDRESSES.EK3, type: 'uint16_t', name: 'OBS_E3K', conversion: '255 * 2.5' },
-  { address: OBS_ADDRESSES.EK4, type: 'uint16_t', name: 'OBS_E4K', conversion: '((LD - LQ) * TPWM_VALUE * MAX_OMEGA_RAD_SEC) / (LD + RS * TPWM_VALUE)' },
-  { address: OBS_ADDRESSES.FOC_KFG, type: 'uint16_t', name: 'FOC_KFG', conversion: 'TIM4频率 / BASE_FREQ / FG_K * 极对数' },
-  { address: OBS_ADDRESSES.SPEED_KLPF, type: 'uint16_t', name: 'SPEED_KLPF', conversion: '32767 * 2PI * SPD_BW * TPWM_VALUE' },
-  { address: OBS_ADDRESSES.OBS_FBASE, type: 'uint16_t', name: 'OBS_FBASE', conversion: '32767 * TPWM_VALUE' },
-  { address: OBS_ADDRESSES.OBS_EA_KS, type: 'uint16_t', name: 'OBS_EA_KS', conversion: '32767 * 2 * SMOMIN * 2PI * BASE_FREQ * TPWM_VALUE / 速度基准' },
-  { address: OBS_ADDRESSES.OBS_KP_START, type: 'uint16_t', name: 'OBS_KP_START', conversion: '4095 * 2PI * ATT_COEF * ATO_BW / BASE_FREQ' },
-  { address: OBS_ADDRESSES.OBS_KI_START, type: 'uint16_t', name: 'OBS_KI_START', conversion: '32767 * 2PI * ATO_BW * ATO_BW * TPWM_VALUE / BASE_FREQ' },
-  { address: OBS_ADDRESSES.OBS_KP_RUN1, type: 'uint16_t', name: 'OBS_KP_RUN1' },
-  { address: OBS_ADDRESSES.OBS_KI_RUN1, type: 'uint16_t', name: 'OBS_KI_RUN1' },
-  { address: OBS_ADDRESSES.OBS_KP_RUN2, type: 'uint16_t', name: 'OBS_KP_RUN2' },
-  { address: OBS_ADDRESSES.OBS_KI_RUN2, type: 'uint16_t', name: 'OBS_KI_RUN2' },
-  { address: OBS_ADDRESSES.OBS_KP_RUN3, type: 'uint16_t', name: 'OBS_KP_RUN3' },
-  { address: OBS_ADDRESSES.OBS_KI_RUN3, type: 'uint16_t', name: 'OBS_KI_RUN3' },
-  { address: OBS_ADDRESSES.OBS_KP_RUN4, type: 'uint16_t', name: 'OBS_KP_RUN4' },
-  { address: OBS_ADDRESSES.OBS_KI_RUN4, type: 'uint16_t', name: 'OBS_KI_RUN4' },
-  { address: OBS_ADDRESSES.DQ_KP_START, type: 'uint16_t', name: 'DQ_KP_START' },
-  { address: OBS_ADDRESSES.DQ_KI_START, type: 'uint16_t', name: 'DQ_KI_START' },
-  { address: OBS_ADDRESSES.DQ_KP_RUN, type: 'uint16_t', name: 'DQ_KP_RUN' },
-  { address: OBS_ADDRESSES.DQ_KI_RUN, type: 'uint16_t', name: 'DQ_KI_RUN' },
-  { address: OBS_ADDRESSES.TAILWIND_SPEED_KLPF, type: 'uint16_t', name: 'SPEED_KLPF_TAILWIND' },
-  { address: OBS_ADDRESSES.TAILWIND_OBS_EA_KS, type: 'uint16_t', name: 'OBS_EA_KS_TAILWIND' },
-  { address: OBS_ADDRESSES.TAILWIND_OBS_KP, type: 'uint16_t', name: 'OBS_KP_TAILWIND' },
-  { address: OBS_ADDRESSES.TAILWIND_OBS_KI, type: 'uint16_t', name: 'OBS_KI_TAILWIND' },
-  { address: OBS_ADDRESSES.ALIGN_ANGLE, type: 'uint16_t', name: '预定位角度' }
-], 'holding', '估算器配置参数')
-
-const atoBandwidthInputRegisters = [
-  { suffix: 'START', name: 'ATO_BW_START', kpAddress: OBS_ADDRESSES.OBS_KP_START, kiAddress: OBS_ADDRESSES.OBS_KI_START, kpName: 'OBS_KP_START', kiName: 'OBS_KI_START' },
-  { suffix: 'RUN1', name: 'ATO_BW_RUN1', kpAddress: OBS_ADDRESSES.OBS_KP_RUN1, kiAddress: OBS_ADDRESSES.OBS_KI_RUN1, kpName: 'OBS_KP_RUN1', kiName: 'OBS_KI_RUN1' },
-  { suffix: 'RUN2', name: 'ATO_BW_RUN2', kpAddress: OBS_ADDRESSES.OBS_KP_RUN2, kiAddress: OBS_ADDRESSES.OBS_KI_RUN2, kpName: 'OBS_KP_RUN2', kiName: 'OBS_KI_RUN2' },
-  { suffix: 'RUN3', name: 'ATO_BW_RUN3', kpAddress: OBS_ADDRESSES.OBS_KP_RUN3, kiAddress: OBS_ADDRESSES.OBS_KI_RUN3, kpName: 'OBS_KP_RUN3', kiName: 'OBS_KI_RUN3' },
-  { suffix: 'RUN4', name: 'ATO_BW_RUN4', kpAddress: OBS_ADDRESSES.OBS_KP_RUN4, kiAddress: OBS_ADDRESSES.OBS_KI_RUN4, kpName: 'OBS_KP_RUN4', kiName: 'OBS_KI_RUN4' },
-  { suffix: 'TAILWIND', name: 'ATO_BW_TAILWIND', kpAddress: OBS_ADDRESSES.TAILWIND_OBS_KP, kiAddress: OBS_ADDRESSES.TAILWIND_OBS_KI, kpName: 'OBS_KP_TAILWIND', kiName: 'OBS_KI_TAILWIND' }
-].map((item) => ({
-  ...item,
-  type: 'float',
-  inputValue: '',
-  writeValue: '--',
-  kpWriteValue: '--',
-  kiWriteValue: '--',
-  category: '估算器配置参数',
-  addressDisplay: `${hex(item.kpAddress)}/${hex(item.kiAddress)}`,
-  area: MODBUS_AREAS.holding
-}))
-
-const dqGainInputRegisters = withArea([
-  { address: OBS_ADDRESSES.DQ_KP_START, type: 'uint16_t', name: 'DQ_KP_START', protocolName: 'DQ_KP_START', gainType: 'kp' },
-  { address: OBS_ADDRESSES.DQ_KI_START, type: 'uint16_t', name: 'DQ_KI_START', protocolName: 'DQ_KI_START', gainType: 'ki' },
-  { address: OBS_ADDRESSES.DQ_KP_RUN, type: 'uint16_t', name: 'DQ_KP_RUN', protocolName: 'DQ_KP_RUN', gainType: 'kp' },
-  { address: OBS_ADDRESSES.DQ_KI_RUN, type: 'uint16_t', name: 'DQ_KI_RUN', protocolName: 'DQ_KI_RUN', gainType: 'ki' }
-], 'holding', '估算器配置参数').map((item) => ({
-  ...item,
-  inputValue: '',
-  writeValue: '--'
-}))
-
-const parameterRegisters = withArea([
-  { address: CONFIG_ADDRESSES.LD, type: 'float', name: 'LD', unit: 'H', step: '0.000001' },
-  { address: CONFIG_ADDRESSES.LQ, type: 'float', name: 'LQ', unit: 'H', step: '0.000001' },
-  { address: CONFIG_ADDRESSES.RS, type: 'float', name: 'RS', unit: 'Ω', step: '0.0001' },
-  { address: CONFIG_ADDRESSES.POLE_PAIRS, type: 'uint16_t', name: '极对数' },
-  { address: CONFIG_ADDRESSES.SPEED_BASE, type: 'uint16_t', name: '速度基准', unit: 'RPM' },
-  { address: CONFIG_ADDRESSES.CURVE_MAX_SPEED, type: 'uint16_t', name: '速度最大值', unit: 'RPM' },
-  { address: CONFIG_ADDRESSES.CURVE_MIN_SPEED, type: 'uint16_t', name: '速度最小值', unit: 'RPM' },
-  { address: CONFIG_ADDRESSES.SOUT_MAX, type: 'uint16_t', name: 'SOUT_MAX', unit: 'A' },
-  { address: CONFIG_ADDRESSES.START_RAMP_INC, type: 'float', name: '启动加速加速度', unit: 'RPM/S' },
-  { address: CONFIG_ADDRESSES.START_RAMP_DEC, type: 'float', name: '启动减速加速度', unit: 'RPM/S' },
-  { address: CONFIG_ADDRESSES.RUN_RAMP_INC, type: 'float', name: '运行加速加速度', unit: 'RPM/S' },
-  { address: CONFIG_ADDRESSES.RUN_RAMP_DEC, type: 'float', name: '运行减速加速度', unit: 'RPM/S' },
-  { address: CONFIG_ADDRESSES.START_VOLT, type: 'uint16_t', name: '开机电压', unit: 'V' },
-  { address: CONFIG_ADDRESSES.STOP_VOLT, type: 'uint16_t', name: '关机电压', unit: 'V' },
-  { address: CONFIG_ADDRESSES.CURVE_MAX_VOLT, type: 'uint16_t', name: '调速最高电压', unit: 'V' },
-  { address: CONFIG_ADDRESSES.CURVE_MIN_VOLT, type: 'uint16_t', name: '调速最低电压', unit: 'V' },
-  { address: CONFIG_ADDRESSES.SPEED_CURVE_K, type: 'float', name: '调速曲线斜率' },
-  { address: CONFIG_ADDRESSES.OIL_SPEED, type: 'uint16_t', name: '上油转速', unit: 'RPM' },
-  { address: CONFIG_ADDRESSES.OIL_TIME, type: 'uint16_t', name: '上油时间', unit: 's' }
-], 'holding', '参数配置')
-
-const parameterInputNames = [
-  'LD',
-  'LQ',
-  'RS',
-  '极对数',
-  '速度基准'
-]
-
-const configParameterInputNames = [
-  '开机电压',
-  '关机电压',
-  '速度最小值',
-  '速度最大值',
-  'SOUT_MAX',
-  '调速最低电压',
-  '调速最高电压'
-]
-
-const speedLoopExtraInputNames = [
-  '启动加速加速度',
-  '启动减速加速度',
-  '运行加速加速度',
-  '运行减速加速度'
-]
-
-const oilParameterInputNames = [
-  '上油转速',
-  '上油时间'
-]
-
-const prepositionParameterInputNames = [
-  '预定位角度'
-]
-
-const parameterByName = parameterRegisters.reduce((result, item) => {
-  result[item.name] = item
-  return result
-}, {})
-const motorParameterInputRegisters = parameterInputNames
-  .map((name) => parameterByName[name])
-  .filter(Boolean)
-  .map((item) => ({
-    ...item,
-    inputValue: item.inputValue || '',
-    writeValue: item.writeValue || '--'
-  }))
-
-const parameterInputRegisters = configParameterInputNames
-  .map((name) => parameterByName[name])
-  .filter(Boolean)
-  .map((item) => ({
-    ...item,
-    inputValue: item.inputValue || '',
-    writeValue: item.writeValue || '--'
-  }))
-
-const speedSlopeRegister = {
-  ...parameterByName['调速曲线斜率'],
-  inputValue: '',
-  writeValue: '--'
-}
-
-const speedLoopExtraRegisters = speedLoopExtraInputNames
-  .map((name) => parameterByName[name])
-  .filter(Boolean)
-  .map((item) => ({
-    ...item,
-    inputValue: '',
-    writeValue: '--'
-  }))
-
-const oilParameterInputRegisters = oilParameterInputNames
-  .map((name) => parameterByName[name])
-  .filter(Boolean)
-  .map((item) => ({
-    ...item,
-    inputValue: '',
-    writeValue: '--'
-  }))
-
-const prepositionParameterInputRegisters = prepositionParameterInputNames
-  .map((name) => estimatorRegisters.find((item) => item.name === name))
-  .filter(Boolean)
-  .map((item) => ({
-    ...item,
-    inputValue: '',
-    writeValue: '--'
-  }))
-
-function isAtoGainRegister(item) {
-  return item.name.startsWith('OBS_KP_') || item.name.startsWith('OBS_KI_')
-}
-
-function isDqGainRegister(item) {
-  return item.name.startsWith('DQ_KP_') || item.name.startsWith('DQ_KI_')
-}
-
-const calculatedParameterRegisters = [
-  ...estimatorRegisters.filter((item) => !isAtoGainRegister(item) && !isDqGainRegister(item) && !prepositionParameterInputNames.includes(item.name)),
-  ...parameterRegisters.filter((item) => !parameterInputNames.includes(item.name)
-    && !configParameterInputNames.includes(item.name)
-    && !speedLoopExtraInputNames.includes(item.name)
-    && !oilParameterInputNames.includes(item.name)
-    && !prepositionParameterInputNames.includes(item.name)
-    && item.name !== '调速曲线斜率')
-].map((item) => ({
-  ...item,
-  inputValue: '',
-  writeValue: '--'
-}))
-
-const protectionRegisters = withArea([
-  { address: '7C', type: 'uint16_t', name: '硬件过流值', unit: 'A' },
-  { address: '7D', type: 'uint16_t', name: '软件过流值', unit: 'A', conversion: '32767 * 限制值 / 电流基准' },
-  { address: '7E', type: 'uint16_t', name: '过压保护值', unit: 'V', conversion: '32767 * 限制值 / 电压采样最大值' },
-  { address: '7F', type: 'uint16_t', name: '欠压保护值', unit: 'V' },
-  { address: '80', type: 'uint16_t', name: '过压恢复值', unit: 'V' },
-  { address: '81', type: 'uint16_t', name: '欠压恢复值', unit: 'V' },
-  { address: '82', type: 'uint16_t', name: '速度限制最大值', unit: 'RPM', conversion: '32767 * 限制值 / 速度基准' },
-  { address: '83', type: 'uint16_t', name: '速度限制最小值', unit: 'RPM' },
-  { address: '84', type: 'uint16_t', name: '反电动势低阈值' },
-  { address: '85', type: 'uint16_t', name: '反电动势高阈值' },
-  { address: '86', type: 'uint16_t', name: '速度中间值', unit: 'RPM' },
-  { address: '87', type: 'uint16_t', name: '功率保护值', unit: 'W', conversion: '32767 * 保护值 / 电流采样最大值 / 电压采样最大值' },
-  { address: '88', type: 'uint16_t', name: '功率保护时间', unit: 'ms' },
-  { address: '89', type: 'uint16_t', name: '温度保护值', unit: '℃' },
-  { address: '8A', type: 'uint16_t', name: '温度恢复值', unit: '℃' },
-  { address: '8B', type: 'uint16_t', name: '温度保护时间', unit: 'ms' },
-  { address: '8C', type: 'uint16_t', name: '故障恢复时间', unit: 's' },
-  { address: '8D', type: 'uint16_t', name: '串口丢失检测时间', unit: 'ms' }
-], 'holding', '保护配置')
-
-const readonlyParamRegisters = withArea([
-  { address: 'A8', type: 'ascii', name: '芯片型号', displayValue: '--', registerCount: 4, hideMeta: true },
-  { address: 'AC', type: 'ascii', name: '型号', displayValue: '--', registerCount: 8, hideMeta: true },
-  { address: 'A0', type: 'uint8_t', name: '载波频率', unit: 'KHz', displayValue: '--', bytePosition: 'high' },
-  { address: 'A0', type: 'uint8_t', name: '基准电压', unit: 'V', displayValue: '--', bytePosition: 'low' },
-  { address: 'A1', type: 'uint16_t', name: '运放倍数', displayValue: '--' },
-  { address: 'A2', type: 'uint16_t', name: '采样电阻', unit: 'mΩ', displayValue: '--' },
-  { address: 'A3', type: 'uint16_t', name: '全区 Flash 校验码', displayValue: '--' },
-  { address: 'A4', type: 'float', name: '母线电压分压比', displayValue: '--' },
-  { address: 'A6', type: 'float', name: '模拟输入电压分压比', displayValue: '--' }
-], 'input', '只读参数寄存器')
-
-const userStatusRegisters = Array.from({ length: 10 }, (_, index) => ({
-  address: (0xD3 + index).toString(16).toUpperCase(),
-  type: 'uint16_t',
-  name: `用户状态字 ${index + 1}`
-}))
-
-const statusRegisters = withArea([
-  { address: 'C0', type: 'uint8_t', name: '状态机', bytePosition: 'high' },
-  { address: 'C0', type: 'uint8_t', name: '故障码', bytePosition: 'low' },
-  { address: 'C1', type: 'int16_t', name: 'UQ' },
-  { address: 'C2', type: 'int16_t', name: 'UD' },
-  { address: 'C3', type: 'int16_t', name: 'IQ' },
-  { address: 'C4', type: 'int16_t', name: 'ID' },
-  { address: 'C5', type: 'int16_t', name: 'A 相电流' },
-  { address: 'C6', type: 'int16_t', name: 'B 相电流' },
-  { address: 'C7', type: 'int16_t', name: 'C 相电流' },
-  { address: 'C8', type: 'uint16_t', name: '相电流最大值' },
-  { address: 'C9', type: 'uint16_t', name: '相电流最小值' },
-  { address: 'CA', type: 'int16_t', name: '估算速度', unit: 'RPM' },
-  { address: 'CB', type: 'uint16_t', name: '估算反电动势' },
-  { address: 'CC', type: 'uint16_t', name: '母线电压', unit: '0.1V', displayUnit: 'V' },
-  { address: 'CD', type: 'uint16_t', name: '母线电流', unit: '0.01A', displayUnit: 'A' },
-  { address: 'CE', type: 'uint16_t', name: '估算功率', unit: 'W' },
-  { address: 'CF', type: 'uint16_t', name: 'NTC 温度', unit: '℃', displayUnit: '℃' },
-  { address: 'D0', type: 'uint16_t', name: '模拟输入电压', unit: 'V', displayUnit: 'V' },
-  { address: 'D1', type: 'uint16_t', name: '频率', unit: 'Hz', displayUnit: 'Hz' },
-  { address: 'D2', type: 'uint16_t', name: '占空比', unit: '%', displayUnit: '%' },
-  ...userStatusRegisters
-], 'input', '状态类寄存器')
-
-function getByteRegisterValue(item, wordValue) {
-  if (!item || item.type !== 'uint8_t' || !item.bytePosition) return wordValue
-
-  return (Number(wordValue) >> item.byteShift) & 0xFF
-}
-
-module.exports = {
-  controlButtonRegisters,
-  speedCommandRegister,
-  atoBandwidthInputRegisters,
-  dqGainInputRegisters,
-  oilParameterInputRegisters,
-  prepositionParameterInputRegisters,
-  motorParameterInputRegisters,
-  parameterInputRegisters,
-  calculatedParameterRegisters,
-  protectionSwitchRegisters,
-  protectionRegisters,
-  readonlyParamRegisters,
-  speedLoopExtraRegisters,
-  speedSlopeRegister,
-  statusRegisters,
-  tailwindSwitchRegisters,
-  getByteRegisterValue,
-  getRegisterCount
-}

+ 0 - 109
domain/motor-control/status-format.js

@@ -1,109 +0,0 @@
-const {
-  getFaultText
-} = require('./calculation-context.js')
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const {
-  calculateStatusValue,
-  formatFixedValue
-} = require('./conversions.js')
-const {
-  getByteRegisterValue
-} = require('./registers.js')
-
-const statusWordValues = {}
-
-const statusRawValues = {}
-
-const SYS_STATE_TEXT = {
-  0: '就绪状态',
-  1: '初始化',
-  2: '检测偏置电压',
-  3: '预充电',
-  4: '风向检测',
-  5: '初始位置检测',
-  6: '预定位',
-  7: '电机启动',
-  8: '电机运行',
-  9: '电机停止',
-  10: '刹车',
-  11: '故障'
-}
-
-const FORMULA_STATUS_NAMES = [
-  '母线电压',
-  '模拟输入电压',
-  '母线电流',
-  'NTC 温度',
-  '估算速度',
-  '估算功率',
-  '频率',
-  '占空比'
-]
-
-function formatStatusRegister(item) {
-  const rawValue = item.type === 'uint8_t' && item.bytePosition && statusWordValues[item.address] !== undefined
-    ? getByteRegisterValue(item, statusWordValues[item.address])
-    : statusRawValues[item.name]
-  const hasFormula = FORMULA_STATUS_NAMES.includes(item.name)
-  let displayValue = rawValue === undefined ? '--' : String(rawValue)
-
-  if (item.name === '故障码' && rawValue !== undefined) {
-    displayValue = getFaultText(rawValue)
-  }
-
-  if (item.name === '状态机' && rawValue !== undefined) {
-    displayValue = SYS_STATE_TEXT[rawValue] || `未知状态 ${rawValue}`
-  }
-
-  if (hasFormula && rawValue !== undefined) {
-    const calculatedValue = calculateStatusValue(item.name, rawValue)
-    displayValue = item.name === '频率' || item.name === '占空比'
-      ? formatFixedValue(calculatedValue, 1)
-      : formatFixedValue(calculatedValue, 2)
-  }
-
-  return {
-    ...item,
-    rawValue: rawValue === undefined ? '--' : rawValue,
-    displayUnit: displayValue === '--' ? '' : item.displayUnit || item.unit || '',
-    displayValue
-  }
-}
-
-function formatStatusRegisters(registers) {
-  return registers.map(formatStatusRegister)
-}
-
-function toSigned16(value) {
-  const wordValue = Number(value) & 0xFFFF
-
-  return wordValue > 0x7FFF ? wordValue - 0x10000 : wordValue
-}
-
-function updateStatusRegisterWords(registers, startAddress, words) {
-  const start = Number(startAddress)
-
-  registers.forEach((item) => {
-    const offset = parseHexInteger(item.address) - start
-    if (offset < 0 || offset >= words.length) return
-
-    const wordValue = Number(words[offset]) & 0xFFFF
-    statusWordValues[item.address] = wordValue
-
-    if (item.type === 'uint8_t' && item.bytePosition) {
-      statusRawValues[item.name] = getByteRegisterValue(item, wordValue)
-      return
-    }
-
-    statusRawValues[item.name] = item.type === 'int16_t' ? toSigned16(wordValue) : wordValue
-  })
-
-  return formatStatusRegisters(registers)
-}
-
-module.exports = {
-  formatStatusRegisters,
-  updateStatusRegisterWords
-}

+ 0 - 120
domain/motor-control/status-state.js

@@ -1,120 +0,0 @@
-const {
-  statusRegisters
-} = require('./registers.js')
-const {
-  MAX_USER_STATUS_COUNT,
-  getUserStatusCount
-} = require('./control-state.js')
-const {
-  formatStatusRegisters
-} = require('./status-format.js')
-
-const STATUS_SUMMARY_METRICS = [
-  { key: 'speed', name: '估算速度', unit: 'RPM', decimals: 0 },
-  { key: 'voltage', name: '母线电压', unit: 'V', decimals: 1 },
-  { key: 'power', name: '估算功率', unit: 'W', decimals: 1 },
-  { key: 'temperature', name: 'NTC 温度', unit: '℃', decimals: 0 }
-]
-const STATUS_GROUPS = [
-  {
-    key: 'system',
-    title: '状态机 / 故障码',
-    names: ['状态机', '故障码']
-  },
-  {
-    key: 'dqVoltageCurrent',
-    title: 'DQ 电压电流',
-    names: ['UD', 'UQ', 'ID', 'IQ']
-  },
-  {
-    key: 'phaseCurrent',
-    title: '相电流',
-    names: ['A 相电流', 'B 相电流', 'C 相电流', '相电流最大值', '相电流最小值']
-  },
-  {
-    key: 'estimator',
-    title: '估算器状态',
-    names: ['估算速度', '估算反电动势']
-  },
-  {
-    key: 'bus',
-    title: '母线 / 温度',
-    names: ['母线电压', '母线电流', '估算功率', 'NTC 温度']
-  },
-  {
-    key: 'inputPwm',
-    title: '输入 / PWM',
-    names: ['模拟输入电压', '占空比', '频率']
-  }
-]
-
-function getVisibleStatusRegisters(userStatusCount) {
-  const count = getUserStatusCount(userStatusCount)
-
-  return statusRegisters.filter((item) => (
-    item.name.indexOf('用户状态字') !== 0 ||
-    Number(item.name.replace('用户状态字 ', '')) <= count
-  ))
-}
-
-function getStatusPageState(userStatusCount) {
-  const registers = formatStatusRegisters(getVisibleStatusRegisters(userStatusCount))
-  const registerMap = registers.reduce((result, item) => {
-    result[item.name] = item
-    return result
-  }, {})
-
-  return {
-    maxUserStatusCount: MAX_USER_STATUS_COUNT,
-    statusRegisterGroups: STATUS_GROUPS.map((group) => ({
-      key: group.key,
-      title: group.title,
-      registers: group.names.map((name) => registerMap[name]).filter(Boolean)
-    })).filter((group) => group.registers.length),
-    statusRegisters: registers,
-    userStatusRegisters: registers.filter((item) => item.name.indexOf('用户状态字') === 0)
-  }
-}
-
-function getFormattedStatusMap() {
-  const formattedRegisters = formatStatusRegisters(statusRegisters)
-
-  return formattedRegisters.reduce((result, item) => {
-    result[item.name] = item
-    return result
-  }, {})
-}
-
-function formatMetricText(item, unit, decimals) {
-  if (!item || item.displayValue === undefined || item.displayValue === '--') return '--'
-
-  const numberValue = Number(item.displayValue)
-  const displayValue = Number.isFinite(numberValue)
-    ? numberValue.toFixed(decimals)
-    : String(item.displayValue)
-
-  return `${displayValue}${unit}`
-}
-
-function getStatusSummaryState() {
-  const registerMap = getFormattedStatusMap()
-  const stateValue = registerMap['状态机'] && registerMap['状态机'].displayValue
-  const faultValue = registerMap['故障码'] && registerMap['故障码'].displayValue
-  const faultText = faultValue || '--'
-  const isFault = faultText !== '--' && faultText !== '无故障'
-
-  return {
-    faultClass: isFault ? 'is-warning' : '',
-    faultText,
-    metrics: STATUS_SUMMARY_METRICS.map((config) => ({
-      key: config.key,
-      displayText: formatMetricText(registerMap[config.name], config.unit, config.decimals)
-    })),
-    stateText: stateValue || '--'
-  }
-}
-
-module.exports = {
-  getStatusPageState,
-  getStatusSummaryState
-}

+ 0 - 130
domain/motor-control/thermistor.js

@@ -1,130 +0,0 @@
-const PULLUP_RESISTOR_KOHM = 10
-
-const RT_TABLE = [
-  [-40, 345.275], [-39, 322.791], [-38, 301.925], [-37, 282.549],
-  [-36, 264.549], [-35, 247.816], [-34, 232.254], [-33, 217.774],
-  [-32, 204.292], [-31, 191.735], [-30, 180.032], [-29, 169.12],
-  [-28, 158.941], [-27, 149.441], [-26, 140.571], [-25, 132.284],
-  [-24, 124.522], [-23, 117.266], [-22, 110.48], [-21, 104.13],
-  [-20, 98.185], [-19, 92.618], [-18, 87.402], [-17, 82.513],
-  [-16, 77.927], [-15, 73.626], [-14, 69.588], [-13, 65.797],
-  [-12, 62.237], [-11, 58.89], [-10, 55.744], [-9, 52.786],
-  [-8, 50.002], [-7, 47.382], [-6, 44.916], [-5, 42.592],
-  [-4, 40.4], [-3, 38.333], [-2, 36.385], [-1, 34.548],
-  [0, 32.814], [1, 31.179], [2, 29.636], [3, 28.178],
-  [4, 26.8], [5, 25.497], [6, 24.263], [7, 23.096],
-  [8, 21.992], [9, 20.947], [10, 19.958], [11, 19.022],
-  [12, 18.135], [13, 17.294], [14, 16.498], [15, 15.742],
-  [16, 15.025], [17, 14.345], [18, 13.699], [19, 13.086],
-  [20, 12.504], [21, 11.951], [22, 11.426], [23, 10.926],
-  [24, 10.452], [25, 10], [26, 9.57], [27, 9.162],
-  [28, 8.773], [29, 8.402], [30, 8.049], [31, 7.713],
-  [32, 7.393], [33, 7.088], [34, 6.797], [35, 6.52],
-  [36, 6.255], [37, 6.003], [38, 5.762], [39, 5.532],
-  [40, 5.313], [41, 5.103], [42, 4.903], [43, 4.711],
-  [44, 4.529], [45, 4.354], [46, 4.187], [47, 4.027],
-  [48, 3.874], [49, 3.728], [50, 3.588], [51, 3.454],
-  [52, 3.326], [53, 3.203], [54, 3.086], [55, 2.973],
-  [56, 2.865], [57, 2.761], [58, 2.662], [59, 2.567],
-  [60, 2.476], [61, 2.388], [62, 2.304], [63, 2.224],
-  [64, 2.146], [65, 2.072], [66, 2.001], [67, 1.932],
-  [68, 1.866], [69, 1.803], [70, 1.742], [71, 1.684],
-  [72, 1.628], [73, 1.574], [74, 1.522], [75, 1.472],
-  [76, 1.424], [77, 1.378], [78, 1.333], [79, 1.29],
-  [80, 1.249], [81, 1.209], [82, 1.171], [83, 1.134],
-  [84, 1.099], [85, 1.065], [86, 1.032], [87, 1],
-  [88, 0.969], [89, 0.94], [90, 0.911], [91, 0.884],
-  [92, 0.857], [93, 0.831], [94, 0.807], [95, 0.783],
-  [96, 0.76], [97, 0.738], [98, 0.716], [99, 0.695],
-  [100, 0.675], [101, 0.656], [102, 0.637], [103, 0.619],
-  [104, 0.602], [105, 0.585], [106, 0.569], [107, 0.553],
-  [108, 0.538], [109, 0.523], [110, 0.508], [111, 0.495],
-  [112, 0.481], [113, 0.468], [114, 0.456], [115, 0.443],
-  [116, 0.432], [117, 0.42], [118, 0.409], [119, 0.399],
-  [120, 0.388], [121, 0.378], [122, 0.368], [123, 0.359],
-  [124, 0.35], [125, 0.341]
-]
-
-function interpolate(x, x0, y0, x1, y1) {
-  if (x1 === x0) return y0
-
-  return y0 + (x - x0) * (y1 - y0) / (x1 - x0)
-}
-
-function resistanceToTemperature(resistanceKohm) {
-  const resistance = Number(resistanceKohm)
-  if (!Number.isFinite(resistance) || resistance <= 0) return null
-
-  if (resistance >= RT_TABLE[0][1]) return RT_TABLE[0][0]
-  const last = RT_TABLE[RT_TABLE.length - 1]
-  if (resistance <= last[1]) return last[0]
-
-  for (let index = 0; index < RT_TABLE.length - 1; index += 1) {
-    const [temp0, resistance0] = RT_TABLE[index]
-    const [temp1, resistance1] = RT_TABLE[index + 1]
-
-    if (resistance <= resistance0 && resistance >= resistance1) {
-      return interpolate(resistance, resistance0, temp0, resistance1, temp1)
-    }
-  }
-
-  return null
-}
-
-function temperatureToResistance(temperatureCelsius) {
-  const temperature = Number(temperatureCelsius)
-  if (!Number.isFinite(temperature)) return null
-
-  if (temperature <= RT_TABLE[0][0]) return RT_TABLE[0][1]
-  const last = RT_TABLE[RT_TABLE.length - 1]
-  if (temperature >= last[0]) return last[1]
-
-  for (let index = 0; index < RT_TABLE.length - 1; index += 1) {
-    const [temp0, resistance0] = RT_TABLE[index]
-    const [temp1, resistance1] = RT_TABLE[index + 1]
-
-    if (temperature >= temp0 && temperature <= temp1) {
-      return interpolate(temperature, temp0, resistance0, temp1, resistance1)
-    }
-  }
-
-  return null
-}
-
-function voltageRatioToResistance(voltageRatio) {
-  const ratio = Number(voltageRatio)
-  if (!Number.isFinite(ratio) || ratio <= 0 || ratio >= 1) return null
-
-  return PULLUP_RESISTOR_KOHM * ratio / (1 - ratio)
-}
-
-function resistanceToVoltageRatio(resistanceKohm) {
-  const resistance = Number(resistanceKohm)
-  if (!Number.isFinite(resistance) || resistance <= 0) return null
-
-  return resistance / (PULLUP_RESISTOR_KOHM + resistance)
-}
-
-function rawToTemperature(rawValue, scaleMax) {
-  const rawNumber = Number(rawValue)
-  const maxValue = Number(scaleMax)
-  if (!Number.isFinite(rawNumber) || !Number.isFinite(maxValue) || maxValue <= 0) return null
-  if (rawNumber <= 0) return RT_TABLE[RT_TABLE.length - 1][0]
-  if (rawNumber >= maxValue) return RT_TABLE[0][0]
-
-  return resistanceToTemperature(voltageRatioToResistance(rawNumber / maxValue))
-}
-
-function temperatureToRaw(temperatureCelsius, scaleMax) {
-  const resistance = temperatureToResistance(temperatureCelsius)
-  const ratio = resistanceToVoltageRatio(resistance)
-  const maxValue = Number(scaleMax)
-  if (!Number.isFinite(ratio) || !Number.isFinite(maxValue) || maxValue <= 0) return null
-
-  return ratio * maxValue
-}
-
-module.exports = {
-  rawToTemperature,
-  temperatureToRaw
-}

+ 1 - 15
features/bootloader/service.js

@@ -15,9 +15,6 @@ const {
   parseBootloaderResponse,
   toHex
 } = require('../../protocols/bootloader/frame.js')
-const {
-  softReset
-} = require('../motor-control/protocol-service.js')
 const {
   delay
 } = require('../../utils/base-utils.js')
@@ -333,12 +330,6 @@ async function sendBootloaderFrame(frame, label, kind, timeout) {
   return responsePromise ? responsePromise : true
 }
 
-async function sendSoftReset() {
-  return softReset({
-    kind: 'bootloader-soft-reset'
-  })
-}
-
 async function sendHandshakeKeepAlive() {
   if (state.isBootloaderBusy) return false
 
@@ -406,16 +397,11 @@ async function handshakeUntilReady() {
   const frame = buildHandshakeFrame()
 
   setState({
-    bootloaderDetailText: '软复位',
+    bootloaderDetailText: '等待握手',
     bootloaderProgress: 0,
     bootloaderStatusText: '握手中'
   })
 
-  const resetResponse = await sendSoftReset()
-  setState({
-    bootloaderDetailText: resetResponse ? '等待握手' : '软复位未响应,继续握手'
-  })
-
   let lastError = null
   let finished = false
   const responsePromise = waitForResponse('handshake', HANDSHAKE_TIMEOUT_MS, {

+ 34 - 1
features/generic-modbus/poller.js

@@ -1,6 +1,27 @@
 const genericModbusService = require('./service.js')
 
 const POLL_TIMER_ID = '__genericPoll'
+const MEMORY_AREA_ORDER = {
+  DATA: 0,
+  IDATA: 1,
+  XDATA: 2,
+  CODE: 3,
+  BIT: 4
+}
+
+function getMemoryAreaOrder(group = {}) {
+  const key = String(group.sourceMemoryArea || '').trim().toUpperCase()
+
+  return Object.prototype.hasOwnProperty.call(MEMORY_AREA_ORDER, key) ? MEMORY_AREA_ORDER[key] : 99
+}
+
+function getGroupAddress(group = {}) {
+  const sourceAddress = Number(group.sourceAddress)
+  if (Number.isFinite(sourceAddress)) return Math.max(0, Math.floor(sourceAddress))
+
+  const startAddress = Number(group.startAddress)
+  return Number.isFinite(startAddress) ? Math.max(0, Math.floor(startAddress)) : 0
+}
 
 function shouldPoll(data) {
   return !!data
@@ -10,7 +31,19 @@ function shouldPoll(data) {
 }
 
 function getPollableGroups(data) {
-  return (data.genericModbusGroups || []).filter((group) => group.isReadOnly && !group.addressOverflow)
+  return (data.genericModbusGroups || [])
+    .filter((group) => {
+      if (!group || group.addressOverflow) return false
+      if ((group.registers || []).some((register) => register && register.isDirty)) return false
+      if (data.isPrivateProtocol) return !!group.sourceMemoryArea
+
+      return group.isReadOnly
+    })
+    .sort((left, right) => (
+      getMemoryAreaOrder(left) - getMemoryAreaOrder(right)
+      || getGroupAddress(left) - getGroupAddress(right)
+      || String(left.name || '').localeCompare(String(right.name || ''))
+    ))
 }
 
 function createGenericModbusPoller(getData) {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 878 - 27
features/generic-modbus/service.js


+ 0 - 35
features/home/service.js

@@ -6,9 +6,7 @@ const {
   getHomePageState
 } = require('./view-model.js')
 
-let syncService = null
 let initScheduled = false
-const syncSubscriptionHooks = []
 
 function deferStartupWork(task) {
   if (typeof task !== 'function') return
@@ -21,23 +19,6 @@ function deferStartupWork(task) {
   task()
 }
 
-function getSyncService() {
-  if (!syncService) {
-    syncService = require('../motor-control/sync-service.js')
-    syncSubscriptionHooks.slice().forEach((hook) => {
-      hook(syncService)
-    })
-  }
-
-  return syncService
-}
-
-function getSyncState() {
-  return syncService ? syncService.getState() : {
-    isSyncing: false
-  }
-}
-
 function init() {
   if (initScheduled) return
 
@@ -53,7 +34,6 @@ function getState(deviceFilterMode = DEFAULT_DEVICE_FILTER) {
   return getHomePageState(
     transport.getState(),
     deviceFilterMode,
-    getSyncState(),
     themeService.getState(),
     manualRtuService.getState()
   )
@@ -66,15 +46,6 @@ function subscribeState(getDeviceFilterMode, subscriber) {
     ? getDeviceFilterMode
     : () => DEFAULT_DEVICE_FILTER
   const emit = () => subscriber(getState(getFilterMode()))
-  let syncUnsubscribe = null
-  const syncHook = (service) => {
-    if (!syncUnsubscribe) {
-      syncUnsubscribe = service.subscribe(emit)
-    }
-  }
-
-  syncSubscriptionHooks.push(syncHook)
-  if (syncService) syncHook(syncService)
 
   const unsubscribers = [
     transport.subscribe(emit),
@@ -86,11 +57,6 @@ function subscribeState(getDeviceFilterMode, subscriber) {
     unsubscribers.forEach((unsubscribe) => {
       if (typeof unsubscribe === 'function') unsubscribe()
     })
-
-    if (typeof syncUnsubscribe === 'function') syncUnsubscribe()
-
-    const index = syncSubscriptionHooks.indexOf(syncHook)
-    if (index >= 0) syncSubscriptionHooks.splice(index, 1)
   }
 }
 
@@ -119,7 +85,6 @@ module.exports = {
   setProtocolMultipleValue: manualRtuService.setProtocolMultipleValue,
   setSendHex: transport.setSendHex,
   subscribeState,
-  syncRegisters: () => getSyncService().syncAllRegisters(),
   toggleScan,
   validateProtocolMultipleValue: manualRtuService.validateProtocolMultipleValue
 }

+ 0 - 5
features/home/view-model.js

@@ -21,7 +21,6 @@ function filterDevices(devices, filterMode) {
 function getHomePageState(
   transportState = transport.getState(),
   deviceFilterMode = DEFAULT_DEVICE_FILTER,
-  syncState = {},
   themeState = themeService.getState(),
   manualRtuState = manualRtuService.getState()
 ) {
@@ -41,9 +40,6 @@ function getHomePageState(
     canClearDevices: !!allDeviceCount && !transportState.isConnecting,
     canDisconnectDevice: !!connectedDevice,
     canStartScan: !transportState.isConnecting,
-    canSyncRegisters: !!connectedDevice
-      && !transportState.isConnecting
-      && !syncState.isSyncing,
     connectionCharacteristicText: connectedDevice ? transportState.characteristicText : '--',
     connectionDeviceId: connectedDevice ? connectedDevice.deviceId : '--',
     connectionName: connectedDevice ? connectedDevice.displayName : '',
@@ -62,7 +58,6 @@ function getHomePageState(
     emptyDeviceTitle: allDeviceCount && deviceFilterMode === 'target'
       ? '没有匹配目标特征的设备'
       : '还没有发现设备',
-    isSyncing: !!syncState.isSyncing,
     scanButtonText: transportState.isDiscovering ? '停止' : '扫描',
     showDeviceSection: transportState.isDiscovering
   }

+ 0 - 459
features/motor-control/control-service.js

@@ -1,459 +0,0 @@
-const {
-  controlState
-} = require('../../domain/motor-control/data.js')
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const transport = require('../../transport/ble-core.js')
-const bootloaderService = require('../bootloader/service.js')
-const settingsService = require('../../store/settings-store.js')
-const modbusClient = require('../../protocols/modbus-rtu/client.js')
-const motorControlProtocol = require('./protocol-service.js')
-
-let state = {
-  ...controlState.createInitialState(),
-  ...bootloaderService.getState()
-}
-let autoReadTimer = null
-let unsubscribeSettings = null
-let unsubscribeTransport = null
-const subscribers = []
-
-function getState() {
-  return {
-    ...state
-  }
-}
-
-function notify() {
-  const nextState = getState()
-
-  subscribers.slice().forEach((subscriber) => {
-    subscriber(nextState)
-  })
-}
-
-function setState(changedData) {
-  state = {
-    ...state,
-    ...changedData
-  }
-  notify()
-}
-
-function applySettingsState(settings) {
-  const autoReadInterval = controlState.clampNumber(
-    settings.statusPollInterval,
-    controlState.AUTO_READ_MIN_INTERVAL,
-    controlState.AUTO_READ_MAX_INTERVAL,
-    state.autoReadInterval
-  )
-  const userStatusCount = controlState.getUserStatusCount(settings.userStatusCount)
-
-  setState({
-    autoReadInterval,
-    userStatusCount
-  })
-
-  if (state.autoReadStatus) {
-    scheduleAutoReadStatus(autoReadInterval)
-  }
-}
-
-function subscribe(subscriber) {
-  if (typeof subscriber !== 'function') return () => {}
-
-  subscribers.push(subscriber)
-  subscriber(getState())
-
-  return () => {
-    const index = subscribers.indexOf(subscriber)
-    if (index >= 0) subscribers.splice(index, 1)
-  }
-}
-
-function init() {
-  transport.init()
-  bootloaderService.init()
-  settingsService.init()
-
-  if (!unsubscribeSettings) {
-    unsubscribeSettings = settingsService.subscribe(applySettingsState)
-  }
-
-  if (unsubscribeTransport) return
-
-  unsubscribeTransport = transport.subscribe((transportState) => {
-    const nextState = controlState.applyTransportState(state, transportState)
-
-    if (nextState.autoReadStatus === false) {
-      stopAutoReadStatus()
-    }
-
-    setState(nextState)
-  })
-
-  bootloaderService.subscribe((bootloaderState) => {
-    setState(bootloaderState)
-  })
-}
-
-function syncSharedInputs() {
-  controlState.setSharedInputValues(state.motorParameterInputRegisters)
-}
-
-function applyControlReadValues(coilValues) {
-  setState(controlState.applyControlReadValues(state, coilValues))
-}
-
-function applyMotorReadWords(words, startAddress = controlState.MOTOR_PARAM_START_ADDRESS) {
-  const registerWordCache = controlState.getRegisterWordCache(startAddress, words)
-  const motorState = controlState.applyMotorParameterReadValues(state, registerWordCache)
-  const nextState = {
-    ...state,
-    ...motorState
-  }
-
-  setState({
-    ...motorState,
-    ...controlState.applySpeedCommandReadValue(nextState, registerWordCache[0x68])
-  })
-}
-
-function applyDriverReadWords(words) {
-  const changedState = controlState.applyDriverParameterReadValues(state, words)
-  if (changedState.chipModel) {
-    bootloaderService.setChipModel(changedState.chipModel)
-  }
-  setState(changedState)
-}
-
-function applyStatusReadWords(words, startAddress = controlState.STATUS_START_ADDRESS) {
-  setState(controlState.applyStatusReadValues(words, startAddress))
-}
-
-function updateMotorParameterInput(index, value) {
-  setState(controlState.applyMotorParameterInput(state, index, value))
-}
-
-function updateMotorParameterBlur(index, value) {
-  setState(controlState.applyMotorParameterBlur(state, index, value))
-}
-
-function updateSpeedCommandInput(value) {
-  setState(controlState.applySpeedCommandInput(state, value))
-}
-
-function updateSpeedCommandBlur(value) {
-  setState(controlState.applySpeedCommandBlur(state, value))
-  sendSpeedCommand()
-}
-
-function getSpeedCommandWriteWord() {
-  const writeValue = Number(state.speedCommand.writeValue)
-  if (!Number.isFinite(writeValue)) return null
-
-  const word = Math.round(writeValue)
-
-  return word >= 0 && word <= 0xFFFF ? word : null
-}
-
-async function sendSpeedCommand() {
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const writeWord = getSpeedCommandWriteWord()
-  if (writeWord === null) {
-    transport.showCommandAlert('转速命令错误', '请检查转速命令输入值')
-    return false
-  }
-
-  const address = parseHexInteger(state.speedCommand.address)
-  const response = await modbusClient.writeSingleRegister(
-    slaveAddress,
-    address,
-    writeWord,
-    '转速命令',
-    'speed-command-write'
-  )
-
-  if (response) {
-    setState({
-      ...controlState.clearSpeedCommandDirty(state),
-      systemTip: '转速命令已下发'
-    })
-    return true
-  }
-
-  return false
-}
-
-async function sendControlCommand(key) {
-  const button = state.controlButtons
-    .concat(state.controlActionButtons || [])
-    .find((item) => item.key === key)
-  if (!button) return
-
-  const response = await motorControlProtocol.writeControlButton(
-    button,
-    {
-      kind: 'control-write'
-    }
-  )
-
-  if (response) {
-    setState(controlState.applyControlSuccess(state, button))
-  }
-}
-
-async function readControlStatus() {
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const startAddress = 0x00
-  const quantity = 3
-  const coilValues = await modbusClient.readBitValues(
-    slaveAddress,
-    0x01,
-    startAddress,
-    quantity,
-    '控制状态读取',
-    'control-status-read'
-  )
-
-  if (!coilValues) return false
-
-  setState({
-    ...controlState.applyControlReadValues(state, coilValues),
-    systemTip: '控制状态读取完成'
-  })
-
-  return true
-}
-
-async function readMotorParameters() {
-  if (state.isReadingMotor) return
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return
-
-  setState({
-    errorText: '',
-    isReadingMotor: true,
-    systemTip: ''
-  })
-
-  try {
-    const words = await modbusClient.readRegisterWords(
-      slaveAddress,
-      0x03,
-      controlState.MOTOR_PARAM_START_ADDRESS,
-      controlState.MOTOR_PARAM_WORD_COUNT,
-      '电机参数读取',
-      'motor-main-read',
-      { showModal: true }
-    )
-    if (!words) return
-
-    const registerWordCache = controlState.getRegisterWordCache(controlState.MOTOR_PARAM_START_ADDRESS, words)
-
-    setState({
-      ...controlState.applyMotorParameterReadValues(state, registerWordCache),
-      systemTip: '电机参数读取完成'
-    })
-  } finally {
-    setState({
-      isReadingMotor: false
-    })
-  }
-}
-
-async function writeMotorParameters() {
-  if (state.isWritingMotor) return
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return
-
-  const mainWrite = controlState.buildMotorMainWriteValues(state)
-  if (!mainWrite.values) {
-    transport.showCommandAlert('参数错误', mainWrite.errorText)
-    return
-  }
-
-  setState({
-    errorText: '',
-    isWritingMotor: true,
-    systemTip: ''
-  })
-
-  try {
-    const mainResponse = await modbusClient.writeMultipleRegisters(
-      slaveAddress,
-      controlState.MOTOR_PARAM_START_ADDRESS,
-      mainWrite.values,
-      '电机参数写入',
-      'motor-main-write'
-    )
-    if (!mainResponse) return
-
-    setState({
-      ...controlState.clearMotorParameterDirty(state),
-      systemTip: '电机参数写入完成'
-    })
-  } finally {
-    setState({
-      isWritingMotor: false
-    })
-  }
-}
-
-async function readDriverParameters() {
-  if (state.isReadingDriver) return
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return
-
-  setState({
-    errorText: '',
-    isReadingDriver: true,
-    systemTip: ''
-  })
-
-  try {
-    const words = await modbusClient.readRegisterWords(
-      slaveAddress,
-      0x04,
-      controlState.DRIVER_PARAM_START_ADDRESS,
-      controlState.DRIVER_PARAM_WORD_COUNT,
-      '驱动器参数读取',
-      'driver-read',
-      { showModal: true }
-    )
-
-    if (words) {
-      const changedState = controlState.applyDriverParameterReadValues(state, words)
-      if (changedState.chipModel) {
-        bootloaderService.setChipModel(changedState.chipModel)
-      }
-      setState({
-        ...changedState,
-        systemTip: '驱动器参数读取完成'
-      })
-    }
-  } finally {
-    setState({
-      isReadingDriver: false
-    })
-  }
-}
-
-function chooseFirmwareFile(source) {
-  return bootloaderService.chooseFirmwareFile(source)
-}
-
-function startFirmwareUpgrade() {
-  stopAutoReadStatus()
-  setState({
-    autoReadStatus: false
-  })
-
-  return bootloaderService.startUpgrade()
-}
-
-function readProgramChecksum() {
-  return bootloaderService.readProgramChecksum()
-}
-
-function handshakeBootloader() {
-  return bootloaderService.sendHandshakeKeepAlive()
-}
-
-function exitBootloader() {
-  return bootloaderService.exitBootloader()
-}
-
-function setAutoReadStatus(autoReadStatus) {
-  setState({
-    autoReadStatus
-  })
-
-  if (autoReadStatus) {
-    scheduleAutoReadStatus(0)
-    return
-  }
-
-  stopAutoReadStatus()
-}
-
-async function readStatus(options = {}) {
-  if (options.auto && !state.connectedDevice) return false
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const words = await modbusClient.readRegisterWords(
-    slaveAddress,
-    0x04,
-    controlState.STATUS_START_ADDRESS,
-    controlState.getStatusWordCount(state.userStatusCount),
-    '状态读取',
-    'status-read',
-    { showModal: !options.auto }
-  )
-
-  if (words) {
-    setState({
-      ...controlState.applyStatusReadValues(words, controlState.STATUS_START_ADDRESS),
-      systemTip: options.auto ? '' : '状态读取完成'
-    })
-  }
-
-  return words
-}
-
-function scheduleAutoReadStatus(delay) {
-  stopAutoReadStatus()
-
-  autoReadTimer = setTimeout(async () => {
-    if (!state.autoReadStatus) return
-
-    await readStatus({
-      auto: true
-    })
-    scheduleAutoReadStatus(state.autoReadInterval)
-  }, delay)
-}
-
-function stopAutoReadStatus() {
-  if (!autoReadTimer) return
-
-  clearTimeout(autoReadTimer)
-  autoReadTimer = null
-}
-
-module.exports = {
-  getState,
-  init,
-  applyControlReadValues,
-  applyDriverReadWords,
-  applyMotorReadWords,
-  applyStatusReadWords,
-  exitBootloader,
-  handshakeBootloader,
-  readControlStatus,
-  readDriverParameters,
-  readMotorParameters,
-  readProgramChecksum,
-  readStatus,
-  sendControlCommand,
-  setAutoReadStatus,
-  chooseFirmwareFile,
-  startFirmwareUpgrade,
-  subscribe,
-  syncSharedInputs,
-  updateMotorParameterBlur,
-  updateMotorParameterInput,
-  updateSpeedCommandBlur,
-  updateSpeedCommandInput,
-  writeMotorParameters
-}

+ 0 - 21
features/motor-control/control-view-model.js

@@ -1,21 +0,0 @@
-const controlService = require('./control-service.js')
-const themeService = require('../../store/theme-store.js')
-const {
-  getStatusSummaryState
-} = require('../../domain/motor-control/status-state.js')
-
-function getControlPageState(
-  controlState = controlService.getState(),
-  themeState = themeService.getState()
-) {
-  return {
-    ...controlState,
-    ...themeState,
-    canReadStatus: !!controlState.connectedDevice && !controlState.isBootloaderBusy,
-    statusSummary: getStatusSummaryState()
-  }
-}
-
-module.exports = {
-  getControlPageState
-}

+ 0 - 16
features/motor-control/index.js

@@ -1,16 +0,0 @@
-const controlViewModel = require('./control-view-model.js')
-const domain = require('../../domain/motor-control/index.js')
-const paramsViewModel = require('./params-view-model.js')
-
-module.exports = {
-  controlService: require('./control-service.js'),
-  controlViewModel,
-  domain,
-  paramsPageState: domain.data.paramsState,
-  paramsService: require('./params-service.js'),
-  paramsViewModel,
-  protocolService: require('./protocol-service.js'),
-  syncService: require('./sync-service.js'),
-  ...controlViewModel,
-  ...paramsViewModel
-}

+ 0 - 234
features/motor-control/params-service.js

@@ -1,234 +0,0 @@
-const {
-  paramsState: paramsPageState
-} = require('../../domain/motor-control/data.js')
-const {
-  expandItems,
-  getAreaKey,
-  getGroupItems,
-  makeReadSpans,
-  mergeReadValues,
-  parseRegisterAddress
-} = require('../../domain/motor-control/register-groups.js')
-const transport = require('../../transport/ble-core.js')
-const modbusClient = require('../../protocols/modbus-rtu/client.js')
-const {
-  floatToWords,
-  toRegisterWord
-} = require('../../utils/register-value-utils.js')
-
-function hasWriteValue(value) {
-  return value !== '' && value !== undefined && value !== null && value !== '--'
-}
-
-function toWriteNumber(value) {
-  if (!hasWriteValue(value)) return null
-
-  const numberValue = Number(value)
-  if (!Number.isFinite(numberValue)) return null
-
-  return numberValue
-}
-
-async function readGroup(data, groupKey) {
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const items = expandItems(getGroupItems(data, groupKey))
-  const coilItems = items.filter((item) => getAreaKey(item) === 'coil')
-  const holdingItems = items.filter((item) => getAreaKey(item) === 'holding')
-  const inputItems = items.filter((item) => getAreaKey(item) === 'input')
-  const coilSpans = makeReadSpans(coilItems)
-  const holdingSpans = makeReadSpans(holdingItems)
-  const inputSpans = makeReadSpans(inputItems)
-  const readValues = {
-    coils: {},
-    words: {}
-  }
-  let sent = false
-
-  if (coilSpans.length) {
-    sent = true
-    const values = await modbusClient.readSpans(
-      slaveAddress,
-      0x01,
-      coilSpans,
-      '参数读取',
-      'params-read'
-    )
-    if (!values) return false
-    mergeReadValues(readValues, values)
-  }
-
-  if (holdingSpans.length) {
-    sent = true
-    const values = await modbusClient.readSpans(
-      slaveAddress,
-      0x03,
-      holdingSpans,
-      '参数读取',
-      'params-read'
-    )
-    if (!values) return false
-    mergeReadValues(readValues, values)
-  }
-
-  if (inputSpans.length) {
-    sent = true
-    const values = await modbusClient.readSpans(
-      slaveAddress,
-      0x04,
-      inputSpans,
-      '参数读取',
-      'params-read'
-    )
-    if (!values) return false
-    mergeReadValues(readValues, values)
-  }
-
-  if (!sent) {
-    transport.showCommandAlert('参数读取', '当前分组没有可读取的寄存器')
-    return false
-  }
-
-  if (!Object.keys(readValues.coils).length && !Object.keys(readValues.words).length) {
-    return false
-  }
-
-  return paramsPageState.applyReadValues(data, readValues)
-}
-
-async function buildHoldingWriteEntries(slaveAddress, items) {
-  const normalEntries = []
-  const byteGroups = {}
-
-  items.forEach((item) => {
-    if (getAreaKey(item) !== 'holding') return
-
-    if (item.type === 'uint8_t' && item.bytePosition) {
-      const address = parseRegisterAddress(item.address)
-      const group = byteGroups[address] || {
-        address,
-        high: null,
-        low: null
-      }
-      group[item.bytePosition] = item
-      byteGroups[address] = group
-      return
-    }
-
-    const writeNumber = toWriteNumber(item.writeValue)
-    if (writeNumber === null) return
-
-    const words = item.type === 'float'
-      ? floatToWords(writeNumber)
-      : [toRegisterWord(writeNumber)]
-
-    if (!words || words.some((word) => word === null)) return
-
-    normalEntries.push({
-      address: parseRegisterAddress(item.address),
-      label: item.name,
-      values: words
-    })
-  })
-
-  for (const addressText of Object.keys(byteGroups)) {
-    const group = byteGroups[addressText]
-    const highValue = group.high ? toRegisterWord(group.high.writeValue) : null
-    const lowValue = group.low ? toRegisterWord(group.low.writeValue) : null
-
-    if (highValue === null && lowValue === null) continue
-
-    let baseWord = 0
-    if (highValue === null || lowValue === null) {
-      const readWord = await modbusClient.readSingleHoldingWord(
-        slaveAddress,
-        group.address,
-        '读取配对寄存器',
-        'params-pair-read'
-      )
-      if (!Number.isInteger(readWord)) continue
-
-      baseWord = readWord
-    }
-
-    const nextHigh = highValue === null ? ((baseWord >> 8) & 0xFF) : highValue
-    const nextLow = lowValue === null ? (baseWord & 0xFF) : lowValue
-
-    if (nextHigh > 0xFF || nextLow > 0xFF) continue
-
-    normalEntries.push({
-      address: group.address,
-      label: '8位参数',
-      values: [(nextHigh << 8) | nextLow]
-    })
-  }
-
-  return normalEntries.sort((left, right) => left.address - right.address)
-}
-
-async function writeGroup(data, groupKey) {
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const items = expandItems(getGroupItems(data, groupKey))
-  const coilItems = items.filter((item) => getAreaKey(item) === 'coil' && item.isDirty)
-  const holdingItems = items.filter((item) => getAreaKey(item) === 'holding')
-  let sent = false
-
-  for (const item of coilItems) {
-    const checked = Number(item.writeValue) !== 0
-    const address = parseRegisterAddress(item.address)
-
-    sent = true
-    const response = await modbusClient.writeSingleCoil(
-      slaveAddress,
-      address,
-      checked,
-      item.name,
-      'params-coil-write'
-    )
-    if (!response) return false
-  }
-
-  const holdingEntries = await buildHoldingWriteEntries(slaveAddress, holdingItems)
-  for (const entry of holdingEntries) {
-    sent = true
-    const response = await modbusClient.writeMultipleRegisters(
-      slaveAddress,
-      entry.address,
-      entry.values,
-      entry.label,
-      'params-holding-write'
-    )
-    if (!response) return false
-  }
-
-  if (!sent) {
-    transport.showCommandAlert('参数写入', '当前分组没有可写入的参数')
-  }
-
-  return sent
-}
-
-async function writeSwitchRegister(item) {
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null || !item) return false
-
-  const address = parseRegisterAddress(item.address)
-  const checked = Number(item.writeValue) !== 0
-
-  return modbusClient.writeSingleCoil(
-    slaveAddress,
-    address,
-    checked,
-    item.name,
-    'params-switch-write'
-  )
-}
-
-module.exports = {
-  readGroup,
-  writeGroup,
-  writeSwitchRegister
-}

+ 0 - 58
features/motor-control/protocol-service.js

@@ -1,58 +0,0 @@
-const modbusClient = require('../../protocols/modbus-rtu/client.js')
-const {
-  parseHexInteger
-} = require('../../utils/base-utils.js')
-const {
-  controlButtonRegisters
-} = require('../../domain/motor-control/registers.js')
-
-function getControlButton(key) {
-  return controlButtonRegisters.find((item) => item.key === key) || null
-}
-
-function getControlButtonWriteValue(button) {
-  if (!button) return 0
-
-  return button.writeValue
-}
-
-async function writeControlButton(button, options = {}) {
-  if (!button) return false
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  const address = parseHexInteger(button.address)
-  const coilEnabled = Number(getControlButtonWriteValue(button)) !== 0
-
-  return modbusClient.writeSingleCoil(
-    slaveAddress,
-    address,
-    coilEnabled,
-    options.label || button.name,
-    options.kind || 'motor-control-write',
-    {
-      showModal: options.showModal
-    }
-  )
-}
-
-function writeControlButtonByKey(key, options = {}) {
-  return writeControlButton(getControlButton(key), options)
-}
-
-function softReset(options = {}) {
-  return writeControlButtonByKey('reset', {
-    label: '软复位',
-    kind: 'motor-control-soft-reset',
-    showModal: false,
-    ...options
-  })
-}
-
-module.exports = {
-  getControlButton,
-  softReset,
-  writeControlButton,
-  writeControlButtonByKey
-}

+ 0 - 182
features/motor-control/sync-service.js

@@ -1,182 +0,0 @@
-const controlService = require('./control-service.js')
-const {
-  controlState,
-  paramsState: paramsPageState
-} = require('../../domain/motor-control/data.js')
-const transport = require('../../transport/ble-core.js')
-const modbusClient = require('../../protocols/modbus-rtu/client.js')
-const {
-  notifyPageToast
-} = require('../../utils/page-toast.js')
-const {
-  addCoilReadValues,
-  addWordReadValues
-} = require('../../utils/register-value-utils.js')
-
-const readValues = {
-  coils: {},
-  words: {}
-}
-
-let paramsSnapshot = paramsPageState.createInitialState()
-let paramsSnapshotVersion = 0
-
-const READ_STEPS = [
-  {
-    address: 0x00,
-    functionCode: 0x01,
-    label: '同步线圈 00-10',
-    quantity: 17,
-    onResponse(response, chunk) {
-      addCoilReadValues(readValues, chunk.address, chunk.quantity, response)
-      controlService.applyControlReadValues(readValues.coils)
-    }
-  },
-  {
-    address: 0x30,
-    functionCode: 0x03,
-    label: '同步估算器参数 30-4A',
-    quantity: 27,
-    onResponse(response, chunk) {
-      addWordReadValues(readValues, chunk.address, response)
-    }
-  },
-  {
-    address: 0x60,
-    functionCode: 0x03,
-    label: '同步参数配置 60-8D',
-    quantity: 46,
-    onResponse(response, chunk) {
-      addWordReadValues(readValues, chunk.address, response)
-      controlService.applyMotorReadWords(response.words || [], chunk.address)
-    }
-  },
-  {
-    address: controlState.DRIVER_PARAM_START_ADDRESS,
-    functionCode: 0x04,
-    label: '同步驱动器参数 A0-B3',
-    quantity: controlState.DRIVER_PARAM_WORD_COUNT,
-    onResponse(response, chunk) {
-      addWordReadValues(readValues, chunk.address, response)
-      controlService.applyDriverReadWords(response.words || [])
-    }
-  },
-  {
-    address: controlState.STATUS_START_ADDRESS,
-    functionCode: 0x04,
-    label: '同步状态',
-    quantity: () => controlState.getStatusWordCount(controlService.getState().userStatusCount),
-    onResponse(response, chunk) {
-      addWordReadValues(readValues, chunk.address, response)
-      controlService.applyStatusReadWords(response.words || [], chunk.address)
-    }
-  }
-]
-
-let syncing = false
-const subscribers = []
-
-function getState() {
-  return {
-    isSyncing: syncing,
-    syncVersion: paramsSnapshotVersion
-  }
-}
-
-function notify() {
-  const state = getState()
-
-  subscribers.slice().forEach((subscriber) => {
-    subscriber(state)
-  })
-}
-
-function setSyncing(value) {
-  syncing = !!value
-  notify()
-}
-
-transport.subscribe((transportState) => {
-  if (!transportState.connectedDevice && syncing) {
-    setSyncing(false)
-  }
-})
-
-function subscribe(subscriber) {
-  if (typeof subscriber !== 'function') return () => {}
-
-  subscribers.push(subscriber)
-  subscriber(getState())
-
-  return () => {
-    const index = subscribers.indexOf(subscriber)
-    if (index >= 0) subscribers.splice(index, 1)
-  }
-}
-
-function resetReadValues() {
-  readValues.coils = {}
-  readValues.words = {}
-}
-
-function getParamsSnapshot() {
-  return {
-    ...paramsSnapshot,
-    syncVersion: paramsSnapshotVersion
-  }
-}
-
-async function syncAllRegisters() {
-  if (syncing) return false
-
-  const transportState = transport.getState()
-  if (!transportState.connectedDevice) return false
-
-  const slaveAddress = modbusClient.getSharedSlaveAddress()
-  if (slaveAddress === null) return false
-
-  setSyncing(true)
-  resetReadValues()
-
-  try {
-    for (const step of READ_STEPS) {
-      const quantity = typeof step.quantity === 'function' ? step.quantity() : step.quantity
-      const values = await modbusClient.readSpans(
-        slaveAddress,
-        step.functionCode,
-        [{
-          address: step.address,
-          quantity
-        }],
-        step.label,
-        'sync-read',
-        {
-          onChunk(response, chunk) {
-            if (typeof step.onResponse === 'function') {
-              step.onResponse(response, chunk)
-            }
-          }
-        }
-      )
-
-      if (!values) return false
-      if (values.coils) Object.assign(readValues.coils, values.coils)
-      if (values.words) Object.assign(readValues.words, values.words)
-    }
-
-    paramsSnapshot = paramsPageState.applyReadValues(paramsSnapshot, readValues)
-    paramsSnapshotVersion += 1
-    notify()
-    notifyPageToast('同步完成')
-    return true
-  } finally {
-    setSyncing(false)
-  }
-}
-
-module.exports = {
-  getParamsSnapshot,
-  getState,
-  subscribe,
-  syncAllRegisters
-}

+ 35 - 117
features/motor-control/params-view-model.js → features/private-protocol/params-view-model.js

@@ -1,149 +1,65 @@
-const controlService = require('./control-service.js')
 const genericModbusService = require('../generic-modbus/service.js')
-const paramsPageState = require('../../domain/motor-control/params-state.js')
 const settingsService = require('../../store/settings-store.js')
-const syncService = require('./sync-service.js')
-const {
-  getStatusPageState
-} = require('../../domain/motor-control/status-state.js')
 const themeService = require('../../store/theme-store.js')
+const transport = require('../../transport/ble-core.js')
 
-const GROUP_LABELS = {
-  dq: 'DQ轴电流环参数',
-  estimator: '估算器参数',
-  oil: '上油参数',
-  preposition: '预定位配置',
-  protection: '保护',
-  speedLoop: '速度环路',
-  tailwind: '顺逆风配置',
-  vsp: 'VSP曲线'
-}
-
-const COMBINED_GROUPS = {
-  speed: ['speedLoop', 'vsp', 'oil'],
-  startup: ['tailwind', 'preposition']
-}
-
-const COMBINED_GROUP_LABELS = {
-  speed: '速度管理',
-  startup: '启动位置管理'
-}
-const PARAM_VIEWS = [
-  'driver',
-  'protection',
-  'estimator',
-  'dq',
-  'startup',
-  'speed',
-  'genericModbus',
-  'genericModbusGroup',
-  'status'
-]
-
-function getGroupLabel(groupKey) {
-  return GROUP_LABELS[groupKey] || '参数'
-}
-
-function getCombinedGroupKeys(viewKey) {
-  return COMBINED_GROUPS[viewKey] || []
-}
-
-function getCombinedGroupLabel(viewKey) {
-  return COMBINED_GROUP_LABELS[viewKey] || '参数'
-}
-
-function hasDirtyItem(items = []) {
-  return items.some((item) => !!item && !!item.isDirty)
-}
-
-function hasWritableGroupChanges(data, groupKey) {
-  if (groupKey === 'tailwind') {
-    return hasDirtyItem(data.tailwindSwitchRegisters) || hasDirtyItem(data.tailwindAtoBandwidthDisplayRegisters)
-  }
-  if (groupKey === 'preposition') {
-    return hasDirtyItem(data.prepositionSwitchRegisters) || hasDirtyItem(data.prepositionParameterDisplayRegisters)
-  }
-  if (groupKey === 'speedLoop') {
-    return hasDirtyItem(data.speedLoopInputDisplayRegisters) || hasDirtyItem(data.speedLoopExtraDisplayRegisters)
-  }
-  if (groupKey === 'vsp') {
-    return hasDirtyItem(data.vspCurveRegisters) || !!(data.speedSlopeRegister && data.speedSlopeRegister.isDirty)
-  }
-  if (groupKey === 'oil') return hasDirtyItem(data.oilParameterInputRegisters)
-
-  return false
-}
-
-function getControlViewState(controlState = controlService.getState()) {
-  return {
-    ...controlState,
-    ...getStatusPageState(controlState.userStatusCount),
-    canReadStatus: !!controlState.connectedDevice
-  }
-}
-
-function getProtocolFlags(settingsState = settingsService.getState()) {
-  return {
-    isGenericProtocol: settingsState.modbusProtocolFilter === 'generic',
-    isMotorControlProtocol: settingsState.modbusProtocolFilter !== 'generic'
-  }
+function getGenericOption(options, index) {
+  return options[Number(index)] || options[0] || {}
 }
 
-function getPageState(
-  paramsState = syncService.getParamsSnapshot(),
-  controlState = controlService.getState()
-) {
+function getPageState() {
   const settingsState = settingsService.getState()
+  const transportState = transport.getState()
+  const isPrivateProtocol = settingsState.modbusProtocolMode !== 'generic'
 
   return {
-    ...paramsPageState.refreshState(paramsState),
-    ...getControlViewState(controlState),
     ...genericModbusService.getState(),
     ...themeService.getState(),
     ...settingsState,
-    ...getProtocolFlags(settingsState)
+    connectedDevice: transportState.connectedDevice,
+    isGenericProtocol: !isPrivateProtocol,
+    isPrivateProtocol
   }
 }
 
-function resolveActiveParamView(currentView, settingsState) {
-  if (settingsState.modbusProtocolFilter === 'generic') {
-    return currentView === 'genericModbus' || currentView === 'genericModbusGroup' ? currentView : 'genericModbus'
-  }
-
-  return PARAM_VIEWS.includes(currentView) && currentView !== 'genericModbus' && currentView !== 'genericModbusGroup' ? currentView : ''
+function resolveActiveParamView(currentView) {
+  return currentView === 'genericModbusGroup' ? currentView : 'genericModbus'
 }
 
 function getSettingsPageState(currentData, settingsState) {
-  const activeParamView = resolveActiveParamView(currentData.activeParamView, settingsState)
+  const isPrivateProtocol = settingsState.modbusProtocolMode !== 'generic'
 
   return {
     ...settingsState,
-    activeParamView,
-    ...getProtocolFlags(settingsState)
+    activeParamView: resolveActiveParamView(currentData.activeParamView),
+    isGenericProtocol: !isPrivateProtocol,
+    isPrivateProtocol
   }
 }
 
 function getVisiblePageState(currentData) {
-  const snapshot = syncService.getParamsSnapshot()
-  const nextParamsState = snapshot.syncVersion && snapshot.syncVersion !== currentData.syncVersion
-    ? paramsPageState.refreshState(snapshot)
-    : paramsPageState.refreshState(currentData)
+  const settingsState = settingsService.getState()
+  const transportState = transport.getState()
+  const isPrivateProtocol = settingsState.modbusProtocolMode !== 'generic'
   const pageState = {
-    ...nextParamsState,
-    ...getControlViewState(),
     ...genericModbusService.getState(),
     ...themeService.getState(),
-    ...settingsService.getState()
+    ...settingsState,
+    connectedDevice: transportState.connectedDevice,
+    isGenericProtocol: !isPrivateProtocol,
+    isPrivateProtocol
   }
+
   return {
     ...pageState,
-    activeParamView: resolveActiveParamView(currentData.activeParamView, pageState),
-    ...getProtocolFlags(pageState)
+    activeParamView: resolveActiveParamView(currentData.activeParamView)
   }
 }
 
-function getGenericOption(options, index) {
-  return options[Number(index)] || options[0] || {}
+function getTransportPageState(transportState = transport.getState()) {
+  return {
+    connectedDevice: transportState.connectedDevice
+  }
 }
 
 function createGenericModbusDialogState(overrides = {}) {
@@ -234,6 +150,7 @@ function createGenericRegisterDialogState(mode, group, register, registerIndex)
     addressText: register.addressRangeText || register.addressText || '',
     displayValue: register.displayValue || '',
     rawValueText: register.rawValueText || '--',
+    sourceMetaText: register.sourceMetaText || '',
     showDataType: !!register.showDataType,
     showRange: !!register.showRange,
     showUnit: !!register.showUnit,
@@ -272,9 +189,14 @@ function createGenericGroupConfig(dialog) {
     startAddress: dialog.startAddress,
     ...(registers.length ? {
       registers: registers.map((register) => ({
+        bitOffset: register.bitOffset,
+        bitWidth: register.bitWidth,
+        byteStart: register.byteStart,
         dataType: register.dataType,
+        isBitField: !!register.isBitField,
         isStructField: true,
         name: register.name,
+        structByteLength: register.structByteLength,
         textByteLength: register.textByteLength
       }))
     } : {})
@@ -324,15 +246,11 @@ module.exports = {
   findGenericGroup,
   findGenericRegister,
   getActiveGenericGroup,
-  getCombinedGroupKeys,
-  getCombinedGroupLabel,
-  getControlViewState,
   getGenericDialogDataTypeState,
   getGenericOption,
-  getGroupLabel,
   getPageState,
   getSettingsPageState,
+  getTransportPageState,
   getVisiblePageState,
-  hasWritableGroupChanges,
   resolveActiveParamView
 }

+ 17 - 20
features/settings/view-model.js

@@ -1,37 +1,35 @@
 const settingsService = require('../../store/settings-store.js')
 const themeService = require('../../store/theme-store.js')
-const controlState = require('../../domain/motor-control/control-state.js')
+const transport = require('../../transport/ble-core.js')
+const bootloaderService = require('../bootloader/service.js')
 const toolNavigation = require('../tools/navigation.js')
 
-function getModbusProtocolMeta(settingsState) {
-  const modbusProtocolOptions = settingsService.MODBUS_PROTOCOL_OPTIONS
-  const modbusProtocolIndex = Math.max(0, modbusProtocolOptions.findIndex((option) => (
-    option.key === settingsState.modbusProtocolFilter
-  )))
-  const modbusProtocolText = (modbusProtocolOptions[modbusProtocolIndex] || modbusProtocolOptions[0]).label
-
-  return {
-    isGenericProtocol: settingsState.modbusProtocolFilter === 'generic',
-    modbusProtocolIndex,
-    modbusProtocolOptions,
-    modbusProtocolText
-  }
-}
-
 function getSettingsPageState(
   settingsState = settingsService.getState(),
-  themeState = themeService.getState()
+  themeState = themeService.getState(),
+  transportState = transport.getState(),
+  bootloaderState = bootloaderService.getState()
 ) {
   const nightModeEnabledSwitch = settingsState.nightModeFollowSystem
     ? themeState.themeMode === 'dark'
     : settingsState.nightModeEnabled
+  const modbusProtocolOptions = settingsService.MODBUS_PROTOCOL_OPTIONS
+  const modbusProtocolIndex = Math.max(0, modbusProtocolOptions.findIndex((option) => (
+    option.key === settingsState.modbusProtocolMode
+  )))
+  const modbusProtocol = modbusProtocolOptions[modbusProtocolIndex] || modbusProtocolOptions[0]
 
   return {
     ...settingsState,
     ...themeState,
-    ...getModbusProtocolMeta(settingsState),
+    ...bootloaderState,
+    connectedDevice: transportState.connectedDevice,
     genericModbusMinPacketLength: settingsService.GENERIC_MODBUS_MIN_PACKET_LENGTH,
-    maxUserStatusCount: controlState.MAX_USER_STATUS_COUNT,
+    isGenericProtocol: modbusProtocol.key === 'generic',
+    isPrivateProtocol: modbusProtocol.key === 'private',
+    modbusProtocolIndex,
+    modbusProtocolOptions,
+    modbusProtocolText: modbusProtocol.label,
     nightModeEnabledSwitch,
     statusPollMaxInterval: settingsService.STATUS_POLL_MAX_INTERVAL,
     statusPollMinInterval: settingsService.STATUS_POLL_MIN_INTERVAL,
@@ -40,6 +38,5 @@ function getSettingsPageState(
 }
 
 module.exports = {
-  getModbusProtocolMeta,
   getSettingsPageState
 }

+ 1 - 0
features/tools/navigation.js

@@ -1,4 +1,5 @@
 const TOOL_ENTRIES = [
+  { view: 'bootloader', label: 'BootLoader升级', icon: 'icon-chip', iconSrc: '/assets/icons/chip-white.png' },
   { view: 'crc', label: 'CRC与哈希计算', icon: 'icon-crc', iconSrc: '/assets/icons/hash-white.png' },
   { view: 'filter', label: '滤波器计算', icon: 'icon-filter', iconSrc: '/assets/icons/funnel-white.png' },
   { view: 'reactance', label: '电抗计算', icon: 'icon-reactance', iconSrc: '/assets/icons/audio-waveform-white.png' },

+ 0 - 6
pages/home/home.js

@@ -145,12 +145,6 @@ Page({
     homeService.toggleScan(this.data.isDiscovering)
   },
 
-  syncRegisters() {
-    if (!this.data.canSyncRegisters) return
-
-    homeService.syncRegisters()
-  },
-
   clearDevices() {
     if (!this.data.canClearDevices) return
 

+ 15 - 11
pages/home/home.wxml

@@ -6,15 +6,11 @@
   <view class="page-shell">
     <view class="connected-panel">
       <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-bluetooth"></view>
+        <view class="panel-icon icon-bluetooth">
+          <image class="panel-icon-image" src="/assets/icons/bluetooth-connected-white.png" mode="aspectFit" />
+        </view>
         <view class="panel-title">连接状态</view>
         <view class="panel-actions connection-actions">
-          <view
-            class="panel-action-button {{canSyncRegisters ? '' : 'is-disabled'}}"
-            bindtap="syncRegisters"
-          >
-            同步
-          </view>
           <view
             class="panel-action-button {{canStartScan ? '' : 'is-disabled'}}"
             bindtap="startScan"
@@ -63,7 +59,9 @@
 
     <view wx:if="{{showDeviceSection}}" class="device-section">
       <view class="panel-header outside-header">
-        <view class="panel-icon icon-radar"></view>
+        <view class="panel-icon icon-radar">
+          <image class="panel-icon-image" src="/assets/icons/radar-white.png" mode="aspectFit" />
+        </view>
         <view class="panel-title">附近设备 {{deviceCountText}}</view>
         <view class="device-filter">
           <view
@@ -113,7 +111,9 @@
 
     <view class="panel">
       <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-terminal"></view>
+        <view class="panel-icon icon-terminal">
+          <image class="panel-icon-image" src="/assets/icons/terminal-white.png" mode="aspectFit" />
+        </view>
         <view class="panel-title">Modbus RTU 指令</view>
         <view class="panel-actions">
           <view
@@ -198,7 +198,9 @@
 
     <view class="panel">
       <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-send"></view>
+        <view class="panel-icon icon-send">
+          <image class="panel-icon-image" src="/assets/icons/send-white.png" mode="aspectFit" />
+        </view>
         <view class="panel-title">发送</view>
         <view class="panel-actions">
           <view class="panel-action-button" bindtap="clearInput">清空</view>
@@ -222,7 +224,9 @@
 
     <view class="panel">
       <view class="panel-header panel-header--with-actions log-header">
-        <view class="panel-icon icon-history"></view>
+        <view class="panel-icon icon-history">
+          <image class="panel-icon-image" src="/assets/icons/history-white.png" mode="aspectFit" />
+        </view>
         <view class="panel-title">收发日志 {{logs.length ? '(' + logs.length + ')' : ''}}</view>
         <view class="panel-actions">
           <view class="panel-action-button" bindtap="clearLogs">清空</view>

+ 0 - 121
pages/index/index.js

@@ -1,121 +0,0 @@
-const themeService = require('../../store/theme-store.js')
-const {
-  controlService,
-  getControlPageState
-} = require('../../features/motor-control/index.js')
-const {
-  createPageToast
-} = require('../../utils/page-toast.js')
-
-Page({
-  data: getControlPageState(),
-
-  onLoad() {
-    this.pageToast = createPageToast(this, this.data)
-    controlService.init()
-    themeService.init()
-    this.unsubscribeControl = controlService.subscribe((controlState) => {
-      const nextState = getControlPageState(controlState)
-
-      this.setData(nextState)
-      this.pageToast.showFromState(nextState)
-    })
-    this.unsubscribeTheme = themeService.subscribe((themeState) => {
-      this.setData(getControlPageState(controlService.getState(), themeState))
-    })
-  },
-
-  onShow() {
-    if (this.pageToast) {
-      this.pageToast.setActive(true)
-    }
-
-    controlService.syncSharedInputs()
-    this.setData(getControlPageState())
-  },
-
-  onHide() {
-    if (this.pageToast) {
-      this.pageToast.setActive(false)
-    }
-  },
-
-  onUnload() {
-    if (this.pageToast) {
-      this.pageToast.destroy()
-      this.pageToast = null
-    }
-
-    if (this.unsubscribeControl) {
-      this.unsubscribeControl()
-      this.unsubscribeControl = null
-    }
-
-    if (this.unsubscribeTheme) {
-      this.unsubscribeTheme()
-      this.unsubscribeTheme = null
-    }
-  },
-
-  readStatus() {
-    if (!this.data.canReadStatus) return
-
-    controlService.readStatus()
-  },
-
-  onAutoReadStatusTap() {
-    if (!this.data.autoReadStatus && !this.data.canReadStatus) return
-
-    controlService.setAutoReadStatus(!this.data.autoReadStatus)
-  },
-
-  onSpeedCommandInput(event) {
-    controlService.updateSpeedCommandInput(event.detail.value)
-  },
-
-  onSpeedCommandBlur(event) {
-    controlService.updateSpeedCommandBlur(event.detail.value)
-  },
-
-  readControlStatus() {
-    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
-
-    controlService.readControlStatus()
-  },
-
-  onControlButtonTap(event) {
-    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
-
-    controlService.sendControlCommand(event.currentTarget.dataset.key)
-  },
-
-  chooseFirmwareFile() {
-    if (this.data.isBootloaderBusy) return
-
-    controlService.chooseFirmwareFile('message')
-  },
-
-  startFirmwareUpgrade() {
-    if (!this.data.connectedDevice || !this.data.isFirmwareReady || this.data.isBootloaderBusy) return
-
-    controlService.startFirmwareUpgrade()
-  },
-
-  readProgramChecksum() {
-    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
-
-    controlService.readProgramChecksum()
-  },
-
-  handshakeBootloader() {
-    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
-
-    controlService.handshakeBootloader()
-  },
-
-  exitBootloader() {
-    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
-
-    controlService.exitBootloader()
-  }
-})

+ 0 - 5
pages/index/index.json

@@ -1,5 +0,0 @@
-{
-  "usingComponents": {
-    "navigation-bar": "/components/navigation-bar/navigation-bar"
-  }
-}

+ 0 - 167
pages/index/index.wxml

@@ -1,167 +0,0 @@
-<navigation-bar background="{{themeMode === 'dark' ? '#111827' : '#FFF'}}"></navigation-bar>
-<view wx:if="{{toastText}}" class="page-toast page-toast--{{toastType}} {{themeClass}}">
-  {{toastText}}
-</view>
-<scroll-view class="scrollarea {{themeClass}}" scroll-y type="list">
-  <view class="page-shell">
-    <view class="panel status-summary-panel">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-status"></view>
-        <view class="panel-title">状态</view>
-        <view class="panel-actions panel-actions--status">
-          <view
-            class="panel-action-button auto-read-button {{autoReadStatus ? 'is-active' : ''}} {{autoReadStatus || canReadStatus ? '' : 'is-disabled'}}"
-            bindtap="onAutoReadStatusTap"
-          >
-            {{autoReadStatus ? '停止' : '自动'}}
-          </view>
-          <view
-            class="panel-action-button {{canReadStatus ? '' : 'is-disabled'}}"
-            bindtap="readStatus"
-          >
-            读取
-          </view>
-        </view>
-      </view>
-      <view class="status-summary-body">
-        <view class="status-summary-top">
-          <view class="status-summary-box status-summary-box--state">
-            <view class="status-summary-box-label">状态机</view>
-            <view class="status-summary-box-value">{{statusSummary.stateText}}</view>
-          </view>
-          <view class="status-summary-box status-summary-box--fault {{statusSummary.faultClass}}">
-            <view class="status-summary-box-label">故障码</view>
-            <view class="status-summary-box-value">{{statusSummary.faultText}}</view>
-          </view>
-        </view>
-        <view class="status-summary-line status-summary-line--metrics">
-          <view
-            wx:for="{{statusSummary.metrics}}"
-            wx:key="key"
-            class="status-metric"
-          >
-            {{item.displayText}}
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="panel control-panel">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-control"></view>
-        <view class="panel-title">控制指令</view>
-        <view class="panel-actions panel-actions--three control-actions">
-          <view
-            class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            bindtap="readControlStatus"
-          >
-            读取
-          </view>
-          <view
-            wx:for="{{controlActionButtons}}"
-            wx:key="key"
-            class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            data-key="{{item.key}}"
-            bindtap="onControlButtonTap"
-          >
-            {{item.name}}
-          </view>
-        </view>
-      </view>
-      <view class="param-row input-row">
-        <view class="param-main">
-          <view class="param-name">{{speedCommand.name}}</view>
-          <view class="param-meta {{speedCommand.isDirty ? 'param-meta--dirty' : ''}}">{{speedCommand.addressDisplay}} {{speedCommand.writeValue}}</view>
-        </view>
-        <view class="input-wrap">
-          <input
-            class="value-input"
-            type="{{speedCommand.unit ? 'text' : 'digit'}}"
-            placeholder="--"
-            value="{{speedCommand.inputValue}}"
-            bindinput="onSpeedCommandInput"
-            bindblur="onSpeedCommandBlur"
-          />
-        </view>
-      </view>
-      <view class="control-grid">
-        <view wx:for="{{controlButtons}}" wx:key="key" class="control-cell control-cell--{{item.key}}">
-          <button
-            class="control-button control-button--{{item.key}}"
-            disabled="{{!connectedDevice || isBootloaderBusy}}"
-            data-key="{{item.key}}"
-            bindtap="onControlButtonTap"
-          >
-            <text class="control-name">{{item.name}}</text>
-          </button>
-        </view>
-      </view>
-    </view>
-
-    <view class="panel upgrade-panel">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-icon icon-chip"></view>
-        <view class="panel-title">BootLoader</view>
-        <view class="panel-actions upgrade-actions">
-          <view
-            class="panel-action-button {{isBootloaderBusy ? 'is-disabled' : ''}}"
-            bindtap="chooseFirmwareFile"
-          >
-            加载
-          </view>
-          <view
-            class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            bindtap="handshakeBootloader"
-          >
-            握手
-          </view>
-          <view
-            class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            bindtap="readProgramChecksum"
-          >
-            读取
-          </view>
-          <view
-            class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            bindtap="exitBootloader"
-          >
-            退出
-          </view>
-          <view
-            class="panel-action-button {{connectedDevice && isFirmwareReady && !isBootloaderBusy ? '' : 'is-disabled'}}"
-            bindtap="startFirmwareUpgrade"
-          >
-            升级
-          </view>
-        </view>
-      </view>
-      <view class="upgrade-body">
-        <view class="upgrade-row">
-          <text class="upgrade-label">芯片型号</text>
-          <text class="upgrade-value">{{chipModel}}</text>
-        </view>
-        <view class="upgrade-row upgrade-row--file">
-          <view class="upgrade-file-head">
-            <text class="upgrade-label">固件文件</text>
-            <view class="upgrade-file-meta">
-              <text class="upgrade-file-meta-item upgrade-file-meta-item--program">{{deviceProgramCrcText}}</text>
-              <text class="upgrade-file-meta-item upgrade-file-meta-item--checksum">{{firmwareChecksumText}}</text>
-              <text class="upgrade-file-meta-item upgrade-file-meta-item--size">{{firmwareSizeText}}</text>
-            </view>
-          </view>
-          <text class="upgrade-file-name">{{firmwareName ? firmwareName : '--'}}</text>
-        </view>
-        <view class="upgrade-row">
-          <text class="upgrade-label">Bootloader</text>
-          <text class="upgrade-value">{{bootloaderVersion}} / {{bootloaderChipId}}</text>
-        </view>
-        <view wx:if="{{bootloaderStatusText || bootloaderDetailText}}" class="upgrade-status {{isFirmwareReady ? 'upgrade-status--ready' : ''}}">
-          {{bootloaderStatusText}}<text wx:if="{{bootloaderDetailText}}"> · {{bootloaderDetailText}}</text>
-        </view>
-        <view class="upgrade-progress">
-          <view class="upgrade-progress-bar" style="width: {{bootloaderProgress}}%;"></view>
-        </view>
-      </view>
-    </view>
-  </view>
-</scroll-view>

+ 0 - 316
pages/index/index.wxss

@@ -1,316 +0,0 @@
-.control-button::after {
-  border: 0;
-}
-
-.control-grid {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 14rpx;
-  padding: 0 20rpx 22rpx;
-}
-
-.control-cell {
-  width: calc((100% - 14rpx) / 2);
-  box-sizing: border-box;
-}
-
-.control-cell--power {
-  width: 100%;
-}
-
-.control-button {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  min-height: 86rpx;
-  margin: 0;
-  padding: 0 20rpx;
-  border: 1rpx solid #d8e2ea;
-  border-radius: 14rpx;
-  background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
-  color: #1f2937;
-  font-size: 26rpx;
-  line-height: 1.3;
-  font-weight: 800;
-  box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
-  box-sizing: border-box;
-}
-
-.control-button:active {
-  opacity: 0.72;
-}
-
-.control-name {
-  min-width: 0;
-  flex: 1;
-  color: #111827;
-  text-align: center;
-  word-break: break-all;
-}
-
-button[disabled].control-button {
-  background: #eef1f5;
-  color: #94a3b8;
-  box-shadow: none;
-}
-
-.status-summary-panel .panel-header {
-  padding-bottom: 16rpx;
-}
-
-.status-summary-body {
-  padding: 0 24rpx 22rpx;
-}
-
-.status-summary-top,
-.status-summary-line {
-  display: flex;
-  align-items: center;
-  gap: 14rpx;
-}
-
-.status-summary-top {
-  padding-top: 2rpx;
-  margin-bottom: 12rpx;
-}
-
-.status-summary-box {
-  flex: 1;
-  min-width: 0;
-  min-height: 88rpx;
-  padding: 14rpx 16rpx 16rpx;
-  border: 1rpx solid #e6ebf2;
-  border-radius: 14rpx;
-  background: #f9fbfd;
-  box-sizing: border-box;
-  overflow: hidden;
-}
-
-.status-summary-box-label {
-  color: #64748b;
-  font-size: 22rpx;
-  line-height: 1.2;
-  font-weight: 700;
-  white-space: nowrap;
-}
-
-.status-summary-box-value {
-  min-width: 0;
-  margin-top: 6rpx;
-  color: #111827;
-  font-size: 27rpx;
-  line-height: 1.35;
-  font-weight: 800;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.status-summary-box--fault.is-warning {
-  border-color: rgba(194, 65, 12, 0.22);
-  background: #fff7ed;
-}
-
-.status-summary-box--fault.is-warning .status-summary-box-value {
-  color: var(--danger);
-}
-
-.status-summary-line--metrics {
-  justify-content: space-between;
-  gap: 8rpx;
-  padding: 14rpx 0 0;
-  border-top: 1rpx solid #edf2f7;
-}
-
-.status-metric {
-  flex: none;
-  width: 160rpx;
-  min-width: 160rpx;
-  color: #111827;
-  font-family: Menlo, Monaco, Consolas, monospace;
-  font-size: 27rpx;
-  line-height: 1.35;
-  font-weight: 800;
-  text-align: center;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  box-sizing: border-box;
-}
-
-.theme-dark .status-summary-box {
-  border-color: #263241;
-  background: #17202c;
-}
-
-.theme-dark .status-summary-box-label {
-  color: #94a3b8;
-}
-
-.theme-dark .status-summary-box-value,
-.theme-dark .status-metric {
-  color: #e5e7eb;
-}
-
-.theme-dark .status-summary-box--fault.is-warning {
-  border-color: #7c2d12;
-  background: #33150f;
-}
-
-.theme-dark .status-summary-box--fault.is-warning .status-summary-box-value {
-  color: #fed7aa;
-}
-
-.theme-dark .status-summary-line--metrics {
-  border-color: #263241;
-}
-
-.upgrade-actions {
-  flex-wrap: wrap;
-  justify-content: flex-end;
-  max-width: 286rpx;
-  row-gap: 8rpx;
-}
-
-.upgrade-body {
-  padding: 8rpx 24rpx 24rpx;
-}
-
-.upgrade-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 20rpx;
-  min-height: 62rpx;
-  border-top: 1rpx solid #edf2f7;
-}
-
-.upgrade-row:first-child {
-  border-top: 0;
-}
-
-.upgrade-row--file {
-  display: block;
-  min-height: 0;
-  padding-top: 14rpx;
-  padding-bottom: 14rpx;
-}
-
-.upgrade-file-head {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 14rpx;
-}
-
-.upgrade-file-meta {
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  gap: 10rpx;
-  min-width: 0;
-  flex: 1;
-  overflow: hidden;
-}
-
-.upgrade-file-meta-item {
-  flex: none;
-  min-width: 0;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.upgrade-file-meta-item--program {
-  color: var(--accent-dark);
-  font-weight: 800;
-}
-
-.upgrade-file-meta-item--checksum,
-.upgrade-file-meta-item--size {
-  color: #64748b;
-}
-
-.upgrade-file-name {
-  display: block;
-  margin-top: 8rpx;
-  color: #111827;
-  font-size: 24rpx;
-  line-height: 1.35;
-  font-weight: 700;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.upgrade-label {
-  flex: none;
-  color: #64748b;
-  font-size: 24rpx;
-  line-height: 1.35;
-}
-
-.upgrade-value {
-  min-width: 0;
-  flex: 1;
-  color: #111827;
-  font-size: 24rpx;
-  line-height: 1.35;
-  text-align: right;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.upgrade-status {
-  margin-top: 14rpx;
-  padding: 14rpx 16rpx;
-  border-radius: 12rpx;
-  background: #f8fafc;
-  color: #64748b;
-  font-size: 23rpx;
-  line-height: 1.45;
-  font-weight: 700;
-  word-break: break-all;
-}
-
-.upgrade-status--ready {
-  background: #effaf8;
-  color: var(--accent-dark);
-}
-
-.upgrade-progress {
-  position: relative;
-  height: 14rpx;
-  margin-top: 16rpx;
-  border-radius: 999rpx;
-  background: #e5e7eb;
-  overflow: hidden;
-}
-
-.upgrade-progress-bar {
-  height: 100%;
-  min-width: 0;
-  border-radius: inherit;
-  background: linear-gradient(90deg, #10b981 0%, #0f8f87 100%);
-  transition: width 0.18s ease;
-}
-
-.theme-dark .upgrade-progress {
-  background: #263241;
-}
-
-.theme-dark .upgrade-progress-bar {
-  background: linear-gradient(90deg, #2dd4bf 0%, #14b8a6 100%);
-}
-
-.theme-dark .upgrade-file-meta-item--checksum,
-.theme-dark .upgrade-file-meta-item--size,
-.theme-dark .upgrade-file-name {
-  color: #cbd5e1;
-}
-
-.theme-dark .upgrade-file-meta-item--program {
-  color: #5eead4;
-}

+ 72 - 280
pages/params/params.js

@@ -1,5 +1,4 @@
 const {
-  controlService,
   createGenericGroupConfig,
   createGenericGroupDialogState,
   createGenericModbusDialogState,
@@ -8,27 +7,21 @@ const {
   findGenericGroup,
   findGenericRegister,
   getActiveGenericGroup,
-  getCombinedGroupKeys,
-  getCombinedGroupLabel,
-  getControlViewState,
   getGenericDialogDataTypeState,
   getGenericOption,
-  getGroupLabel,
   getPageState,
   getSettingsPageState,
+  getTransportPageState,
   getVisiblePageState,
-  hasWritableGroupChanges,
-  paramsPageState,
-  paramsService,
-  resolveActiveParamView,
-  syncService
-} = require('../../features/motor-control/index.js')
+  resolveActiveParamView
+} = require('../../features/private-protocol/params-view-model.js')
 const {
   createGenericModbusPoller,
   genericModbusService
 } = require('../../features/generic-modbus/index.js')
 const settingsService = require('../../store/settings-store.js')
 const themeService = require('../../store/theme-store.js')
+const transport = require('../../transport/ble-core.js')
 const {
   createPageToast
 } = require('../../utils/page-toast.js')
@@ -148,7 +141,7 @@ function buildActiveGenericRegisterRows(group, dragState) {
 Page({
   data: {
     ...getPageState(),
-    activeParamView: '',
+    activeParamView: 'genericModbus',
     activeGenericGroupId: '',
     activeGenericRegisterRows: [],
     genericModbusDialog: createGenericModbusDialogState()
@@ -163,35 +156,20 @@ Page({
     this.genericModbusPoller = createGenericModbusPoller(() => this.data)
     this.genericModbusTouchStarts = {}
     this.genericWindowWidth = getWindowWidth()
-    controlService.init()
     genericModbusService.init()
     themeService.init()
     settingsService.init()
-    this.unsubscribeSync = syncService.subscribe((syncState) => {
-      if (!syncState.syncVersion || syncState.syncVersion === this.data.syncVersion) return
-
-      const nextState = getPageState(
-        syncService.getParamsSnapshot(),
-        controlService.getState()
-      )
-
-      this.setData(nextState)
-      this.pageToast.showFromState(nextState)
+    this.unsubscribeTheme = themeService.subscribe((themeState) => {
+      this.setData(themeState)
     })
-    this.unsubscribeControl = controlService.subscribe((controlState) => {
-      const nextState = getControlViewState(controlState)
-
-      this.setData(nextState)
-      this.pageToast.showFromState(nextState)
-      if (nextState.connectedDevice) {
-        this.scheduleVisibleGenericAutoReads()
+    this.unsubscribeTransport = transport.subscribe((transportState) => {
+      this.setData(getTransportPageState(transportState))
+      if (transportState.connectedDevice) {
+        setTimeout(() => this.scheduleVisibleGenericAutoReads(), 0)
       } else {
         this.clearGenericAutoTimers()
       }
     })
-    this.unsubscribeTheme = themeService.subscribe((themeState) => {
-      this.setData(themeState)
-    })
     this.unsubscribeGenericModbus = genericModbusService.subscribe((genericState) => {
       const activeGenericGroup = getActiveGenericGroup(genericState.genericModbusGroups, this.data.activeGenericGroupId)
       this.setData({
@@ -230,8 +208,6 @@ Page({
       this.pageToast.setActive(true)
     }
 
-    controlService.syncSharedInputs()
-
     const pageState = getVisiblePageState(this.data)
     this.setData({
       ...pageState,
@@ -259,21 +235,16 @@ Page({
       this.pageToast = null
     }
 
-    if (this.unsubscribeSync) {
-      this.unsubscribeSync()
-      this.unsubscribeSync = null
-    }
-
-    if (this.unsubscribeControl) {
-      this.unsubscribeControl()
-      this.unsubscribeControl = null
-    }
-
     if (this.unsubscribeTheme) {
       this.unsubscribeTheme()
       this.unsubscribeTheme = null
     }
 
+    if (this.unsubscribeTransport) {
+      this.unsubscribeTransport()
+      this.unsubscribeTransport = null
+    }
+
     if (this.unsubscribeGenericModbus) {
       this.unsubscribeGenericModbus()
       this.unsubscribeGenericModbus = null
@@ -287,104 +258,6 @@ Page({
     this.clearGenericAutoTimers()
   },
 
-  async onGroupRead(event) {
-    if (!this.data.connectedDevice) return
-
-    const groupKey = event.currentTarget.dataset.group
-    const nextState = await paramsService.readGroup(this.data, groupKey)
-
-    if (nextState) {
-      this.setData(nextState)
-      if (this.pageToast) this.pageToast.show(`${getGroupLabel(groupKey)}读取完成`)
-    }
-  },
-
-  async onGroupWrite(event) {
-    if (!this.data.connectedDevice) return
-
-    const groupKey = event.currentTarget.dataset.group
-    const written = await paramsService.writeGroup(this.data, groupKey)
-
-    if (written) {
-      this.setData(paramsPageState.clearGroupDirty(this.data, groupKey))
-      if (this.pageToast) this.pageToast.show(`${getGroupLabel(groupKey)}写入完成`)
-    }
-  },
-
-  async readCombinedGroups(viewKey) {
-    if (!this.data.connectedDevice) return false
-
-    const groupKeys = getCombinedGroupKeys(viewKey)
-    let nextState = this.data
-
-    for (const groupKey of groupKeys) {
-      const updatedState = await paramsService.readGroup(nextState, groupKey)
-      if (!updatedState) {
-        if (nextState !== this.data) this.setData(nextState)
-        return false
-      }
-
-      nextState = updatedState
-      this.setData(nextState)
-    }
-
-    if (this.pageToast) this.pageToast.show(`${getCombinedGroupLabel(viewKey)}读取完成`)
-
-    return true
-  },
-
-  async writeCombinedGroups(viewKey) {
-    if (!this.data.connectedDevice) return false
-
-    const groupKeys = getCombinedGroupKeys(viewKey)
-    let nextState = this.data
-    let writtenAny = false
-
-    for (const groupKey of groupKeys) {
-      if (!hasWritableGroupChanges(nextState, groupKey)) continue
-
-      const written = await paramsService.writeGroup(nextState, groupKey)
-      if (!written) {
-        if (writtenAny) this.setData(nextState)
-        return false
-      }
-
-      nextState = paramsPageState.clearGroupDirty(nextState, groupKey)
-      writtenAny = true
-      this.setData(nextState)
-    }
-
-    if (!writtenAny) {
-      if (this.pageToast) this.pageToast.show('暂无需要写入的参数')
-      return false
-    }
-
-    if (this.pageToast) this.pageToast.show(`${getCombinedGroupLabel(viewKey)}写入完成`)
-
-    return true
-  },
-
-  readStartupManagement() {
-    this.readCombinedGroups('startup')
-  },
-
-  writeStartupManagement() {
-    this.writeCombinedGroups('startup')
-  },
-
-  readSpeedManagement() {
-    this.readCombinedGroups('speed')
-  },
-
-  writeSpeedManagement() {
-    this.writeCombinedGroups('speed')
-  },
-
-  onEstimatorUpdate() {
-    this.setData(paramsPageState.refreshState(this.data))
-    if (this.pageToast) this.pageToast.show('估算器参数更新完成')
-  },
-
   openParamView(event) {
     if (this.pageToast) this.pageToast.clear()
     this.closeGenericModbusDraft()
@@ -434,142 +307,6 @@ Page({
     }
   },
 
-  onMotorParameterInput(event) {
-    controlService.updateMotorParameterInput(
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    )
-  },
-
-  onMotorParameterBlur(event) {
-    controlService.updateMotorParameterBlur(
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    )
-  },
-
-  writeMotorParameters() {
-    if (!this.data.connectedDevice) return
-
-    controlService.writeMotorParameters()
-  },
-
-  async readDriverPageParameters() {
-    if (!this.data.connectedDevice) return
-
-    await controlService.readDriverParameters()
-    await controlService.readMotorParameters()
-  },
-
-  readStatus() {
-    if (!this.data.canReadStatus) return
-
-    controlService.readStatus()
-  },
-
-  onInputChange(event) {
-    this.setData(paramsPageState.applyParameterInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onAtoBandwidthInput(event) {
-    this.setData(paramsPageState.applyAtoBandwidthInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onDqGainInput(event) {
-    this.setData(paramsPageState.applyDqGainInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onSpeedLoopExtraInput(event) {
-    this.setData(paramsPageState.applySpeedLoopExtraInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onOilParameterInput(event) {
-    this.setData(paramsPageState.applyOilParameterInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onPrepositionParameterInput(event) {
-    this.setData(paramsPageState.applyPrepositionParameterInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onInputBlur(event) {
-    this.setData(paramsPageState.applyInputBlur(
-      this.data,
-      event.currentTarget.dataset.inputGroup,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
-  onTailwindSwitchChange(event) {
-    if (!this.data.connectedDevice) return
-
-    const index = Number(event.currentTarget.dataset.index)
-    const nextState = paramsPageState.applyTailwindSwitchChange(
-      this.data,
-      index,
-      !!event.detail.value
-    )
-
-    this.setData(nextState)
-    paramsService.writeSwitchRegister(nextState.tailwindSwitchRegisters[index]).then((written) => {
-      if (written) {
-        this.setData(paramsPageState.clearTailwindSwitchDirty(this.data, index))
-        if (this.pageToast) this.pageToast.show(`${nextState.tailwindSwitchRegisters[index].name}写入完成`)
-      }
-    })
-  },
-
-  onProtectionSwitchChange(event) {
-    if (!this.data.connectedDevice) return
-
-    const index = Number(event.currentTarget.dataset.index)
-    const nextState = paramsPageState.applyProtectionSwitchChange(
-      this.data,
-      index,
-      !!event.detail.value
-    )
-
-    this.setData(nextState)
-    paramsService.writeSwitchRegister(nextState.protectionSwitchRegisters[index]).then((written) => {
-      if (written) {
-        this.setData(paramsPageState.clearProtectionSwitchDirty(this.data, index))
-        if (this.pageToast) this.pageToast.show(`${nextState.protectionSwitchRegisters[index].name}写入完成`)
-      }
-    })
-  },
-
-  onProtectionInputChange(event) {
-    this.setData(paramsPageState.applyProtectionInput(
-      this.data,
-      Number(event.currentTarget.dataset.index),
-      event.detail.value
-    ))
-  },
-
   noop() {},
 
   updateGenericModbusDialog(changedData) {
@@ -758,6 +495,45 @@ Page({
     if (count && this.pageToast) this.pageToast.show(`已导入 ${count} 个寄存器组`)
   },
 
+  async syncGenericModbusGroups() {
+    if (this.data.isGenericProtocol) return
+    if (!this.data.connectedDevice) return
+
+    const result = await genericModbusService.queryCodeInfoBlock({
+      maxPacketLength: this.data.genericModbusMaxPacketLength
+    })
+    if (result && result.ok && this.pageToast) {
+      const addressText = Number(result.address || 0).toString(16).toUpperCase().padStart(4, '0')
+      const addedCount = Number(result.addedGroups || 0) + Number(result.addedRegisters || 0)
+      const updatedCount = Number(result.updatedGroups || 0) + Number(result.updatedRegisters || 0)
+      const changedText = [
+        addedCount ? `新增 ${addedCount}` : '',
+        updatedCount ? `更新 ${updatedCount}` : ''
+      ].filter(Boolean).join(',')
+
+      this.pageToast.show(`同步完成 0x${addressText},${result.structCount} 项${changedText ? `,${changedText}` : ''}`)
+    }
+  },
+
+  async completeGenericModbusStructs() {
+    const result = await genericModbusService.completeStructInstanceGroupsWithStructFile()
+    if (result && result.completedCount && this.pageToast) {
+      this.pageToast.show(`已补全 ${result.completedCount} 个寄存器组`)
+    }
+  },
+
+  toggleGenericModbusPolling() {
+    if (this.data.isPrivateProtocol && !this.data.connectedDevice) return
+
+    const enabled = !this.data.genericModbusAutoPollEnabled
+    settingsService.setGenericModbusAutoPollEnabled(enabled)
+    if (enabled) {
+      this.scheduleGenericAutoPoll(0)
+    } else {
+      this.clearGenericAutoTimers()
+    }
+  },
+
   async saveGenericModbusJson() {
     const count = await genericModbusService.saveJsonToChat()
     if (count && this.pageToast) this.pageToast.show(`已保存 ${count} 个寄存器组`)
@@ -783,13 +559,29 @@ Page({
     )
   },
 
-  onGenericRegisterValueBlur(event) {
+  async onGenericRegisterValueBlur(event) {
     const groupId = event.currentTarget.dataset.groupId
     const registerIndex = Number(event.currentTarget.dataset.index)
     try {
       genericModbusService.validateRegisterInputValue(groupId, registerIndex, event.detail.value)
     } catch (error) {
       if (this.pageToast) this.pageToast.show(error.message || '输入值无效', 'error')
+      return
+    }
+
+    if (!this.data.isPrivateProtocol || !this.data.connectedDevice) return
+
+    const group = findGenericGroup(this.data.genericModbusGroups, groupId)
+    const register = group && group.registers ? group.registers[registerIndex] : null
+    if (!group || !register || !register.isDirty || !group.writable || group.addressOverflow) return
+
+    this.clearGenericAutoTimers()
+    const ok = await genericModbusService.writeRegister(groupId, registerIndex)
+    if (this.data.genericModbusAutoPollEnabled) {
+      this.scheduleGenericAutoPoll(this.data.genericModbusPollInterval || 100)
+    }
+    if (ok && this.pageToast) {
+      this.pageToast.show(`${register.name || '变量'}已写入`)
     }
   },
 

+ 20 - 482
pages/params/params.wxml

@@ -2,45 +2,17 @@
 <view wx:if="{{toastText}}" class="page-toast page-toast--{{toastType}} {{themeClass}}">
   {{toastText}}
 </view>
-<view wx:if="{{activeParamView}}" class="subpage-fixed-header {{activeParamView == 'genericModbus' || activeParamView == 'genericModbusGroup' ? 'subpage-fixed-header--generic' : ''}} {{themeClass}}">
+<view class="subpage-fixed-header subpage-fixed-header--generic {{themeClass}}">
   <view class="subpage-page-header">
-    <view wx:if="{{!(isGenericProtocol && activeParamView == 'genericModbus')}}" class="subpage-back" bindtap="backToParamsHome">
-      <view class="subpage-back-icon"></view>
-    </view>
-    <view class="subpage-page-title">
-      {{activeParamView == 'driver' ? '驱动器参数' : activeParamView == 'protection' ? '保护' : activeParamView == 'estimator' ? '估算器参数' : activeParamView == 'dq' ? 'DQ轴电流环参数' : activeParamView == 'startup' ? '启动位置管理' : activeParamView == 'speed' ? '速度管理' : activeParamView == 'genericModbus' ? '' : activeParamView == 'genericModbusGroup' ? (activeGenericGroup.name || '寄存器组') : activeParamView == 'status' ? '状态' : ''}}
-    </view>
-    <view wx:if="{{activeParamView == 'driver'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="readDriverPageParameters">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="writeMotorParameters">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'protection'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="protection" bindtap="onGroupRead">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="protection" bindtap="onGroupWrite">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'estimator'}}" class="panel-actions panel-actions--three subpage-actions">
-      <view class="panel-action-button" bindtap="onEstimatorUpdate">更新</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="estimator" bindtap="onGroupRead">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="estimator" bindtap="onGroupWrite">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'dq'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="dq" bindtap="onGroupRead">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" data-group="dq" bindtap="onGroupWrite">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'startup'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="readStartupManagement">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="writeStartupManagement">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'speed'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="readSpeedManagement">读取</view>
-      <view class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}}" bindtap="writeSpeedManagement">写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'genericModbus'}}" class="panel-actions subpage-actions">
+    <view wx:if="{{activeParamView == 'genericModbus'}}" class="panel-actions subpage-actions generic-protocol-actions">
+      <view class="panel-action-button {{isPrivateProtocol && !connectedDevice ? 'is-disabled' : ''}}" bindtap="syncGenericModbusGroups">同步</view>
+      <view wx:if="{{isPrivateProtocol}}" class="panel-action-button {{connectedDevice ? '' : 'is-disabled'}} {{genericModbusAutoPollEnabled ? 'is-active' : ''}}" bindtap="toggleGenericModbusPolling">轮询</view>
       <view class="panel-action-button" bindtap="saveGenericModbusJson">保存</view>
       <view class="panel-action-button" bindtap="importGenericModbusJson">加载</view>
-      <view class="panel-action-button panel-action-button--icon" bindtap="openGenericModbusDraft">+</view>
+      <view wx:if="{{isPrivateProtocol}}" class="panel-action-button" bindtap="completeGenericModbusStructs">结构</view>
+      <view wx:if="{{isGenericProtocol}}" class="panel-action-button panel-action-button--icon" bindtap="openGenericModbusDraft">+</view>
     </view>
-    <view wx:elif="{{activeParamView == 'genericModbusGroup'}}" class="panel-actions subpage-actions">
+    <view wx:elif="{{activeParamView == 'genericModbusGroup' && isGenericProtocol}}" class="panel-actions subpage-actions">
       <view
         class="panel-action-button {{connectedDevice && !activeGenericGroup.addressOverflow ? '' : 'is-disabled'}}"
         data-group-id="{{activeGenericGroup.id}}"
@@ -52,359 +24,13 @@
         data-group-id="{{activeGenericGroup.id}}"
         bindtap="writeGenericModbusGroup"
       >写入</view>
-    </view>
-    <view wx:elif="{{activeParamView == 'status'}}" class="panel-actions subpage-actions">
-      <view class="panel-action-button {{canReadStatus ? '' : 'is-disabled'}}" bindtap="readStatus">读取</view>
+      <view class="panel-action-button" bindtap="backToParamsHome">返回</view>
     </view>
   </view>
 </view>
-<scroll-view class="scrollarea {{themeClass}} {{activeParamView ? 'scrollarea--subpage' : ''}} {{activeParamView == 'genericModbus' || activeParamView == 'genericModbusGroup' ? 'scrollarea--generic' : ''}}" scroll-y type="list">
+<scroll-view class="scrollarea {{themeClass}} scrollarea--subpage scrollarea--generic" scroll-y type="list">
   <view class="page-shell">
-    <block wx:if="{{activeParamView == 'driver'}}">
-      <view class="panel driver-summary-panel">
-        <view class="driver-summary-row driver-summary-row--top">
-          <text class="driver-summary-chip">{{chipModel || '--'}}</text>
-          <text class="driver-summary-checksum">{{flashChecksum || '--'}}</text>
-        </view>
-        <view class="driver-summary-row driver-summary-row--model">
-          <text class="driver-summary-model">{{motorModel || '--'}}</text>
-        </view>
-      </view>
-
-      <view class="panel params-section-panel">
-        <view class="params-section-title">硬件参数</view>
-        <view wx:for="{{readonlyParamRegisters}}" wx:key="name" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view wx:if="{{!item.hideMeta}}" class="param-meta">{{item.addressDisplay}}</view>
-          </view>
-          <view class="param-value">{{item.displayValue || '--'}}{{item.unit ? ' ' + item.unit : ''}}</view>
-        </view>
-      </view>
-
-      <view class="panel params-section-panel">
-        <view class="params-section-title">电机参数</view>
-        <view wx:for="{{motorParameterInputRegisters}}" wx:key="name" class="param-row input-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input value-input--with-unit"
-              type="{{item.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{item.inputValue}}"
-              data-index="{{index}}"
-              bindinput="onMotorParameterInput"
-              bindblur="onMotorParameterBlur"
-            />
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'protection'}}">
-      <view
-        wx:for="{{protectionSections}}"
-        wx:for-item="section"
-        wx:key="key"
-        class="panel protection-section-panel"
-      >
-        <view class="params-section-title">{{section.title}}</view>
-        <view
-          wx:for="{{section.rows}}"
-          wx:for-item="row"
-          wx:key="key"
-          class="protection-field-row"
-        >
-          <view
-            wx:for="{{row.fields}}"
-            wx:for-item="field"
-            wx:key="name"
-            class="protection-field protection-field--{{field.kind}}"
-          >
-            <view class="protection-field-main">
-              <view class="param-name">{{field.label}}</view>
-              <view class="param-meta {{field.isDirty ? 'param-meta--dirty' : ''}}">{{field.addressDisplay}} {{field.metaValue}}</view>
-            </view>
-            <switch
-              wx:if="{{field.kind == 'switch'}}"
-              checked="{{field.value}}"
-              color="#0f766e"
-              disabled="{{!connectedDevice}}"
-              data-index="{{field.sourceIndex}}"
-              bindchange="onProtectionSwitchChange"
-            />
-            <view wx:else class="input-wrap">
-              <input
-                class="value-input {{field.unit ? 'value-input--with-unit' : ''}}"
-                type="{{field.unit ? 'text' : 'digit'}}"
-                placeholder="--"
-                value="{{field.inputValue}}"
-                data-index="{{field.sourceIndex}}"
-                data-input-group="protection"
-                bindinput="onProtectionInputChange"
-                bindblur="onInputBlur"
-              />
-            </view>
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'estimator'}}">
-      <view class="panel params-section-panel">
-        <view wx:for="{{estimatorCalculatedDisplayRegisters}}" wx:key="name" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-          </view>
-          <view class="param-value">{{item.writeValue || '--'}}{{item.unit ? ' ' + item.unit : ''}}</view>
-        </view>
-        <view
-          wx:for="{{atoBandwidthDisplayRegisters}}"
-          wx:for-item="atoItem"
-          wx:key="name"
-          class="param-row input-row"
-        >
-          <view class="param-main">
-            <view class="param-name">{{atoItem.name}}</view>
-            <view class="param-meta {{atoItem.isDirty ? 'param-meta--dirty' : ''}}">KP 0x{{atoItem.kpAddress}} {{atoItem.kpWriteValue || '--'}}  KI 0x{{atoItem.kiAddress}} {{atoItem.kiWriteValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input {{atoItem.unit ? 'value-input--with-unit' : ''}}"
-              type="{{atoItem.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{atoItem.inputValue}}"
-              data-index="{{atoItem.sourceIndex}}"
-              data-input-group="ato"
-              bindinput="onAtoBandwidthInput"
-              bindblur="onInputBlur"
-            />
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'dq'}}">
-      <view class="panel params-section-panel">
-        <view
-          wx:for="{{dqGainDisplayRegisters}}"
-          wx:for-item="dqItem"
-          wx:key="name"
-          class="param-row input-row"
-        >
-          <view class="param-main">
-            <view class="param-name">{{dqItem.name}}</view>
-            <view class="param-meta {{dqItem.isDirty ? 'param-meta--dirty' : ''}}">{{dqItem.addressDisplay}} {{dqItem.writeValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input {{dqItem.unit ? 'value-input--with-unit' : ''}}"
-              type="{{dqItem.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{dqItem.inputValue}}"
-              data-index="{{dqItem.sourceIndex}}"
-              data-input-group="dq"
-              bindinput="onDqGainInput"
-              bindblur="onInputBlur"
-            />
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'startup'}}">
-      <view class="panel params-section-panel">
-        <view class="params-section-title">顺逆风配置</view>
-        <view wx:for="{{tailwindControlRegisters}}" wx:key="address" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue}}</view>
-          </view>
-          <switch
-            checked="{{item.value}}"
-            color="#0f766e"
-            disabled="{{!connectedDevice}}"
-            data-index="{{item.sourceIndex}}"
-            bindchange="onTailwindSwitchChange"
-          />
-        </view>
-        <view wx:for="{{tailwindCalculatedDisplayRegisters}}" wx:key="name" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-          </view>
-          <view class="param-value">{{item.writeValue || '--'}}{{item.unit ? ' ' + item.unit : ''}}</view>
-        </view>
-        <view
-          wx:for="{{tailwindAtoBandwidthDisplayRegisters}}"
-          wx:for-item="atoItem"
-          wx:key="name"
-          class="param-row input-row"
-        >
-          <view class="param-main">
-            <view class="param-name">{{atoItem.name}}</view>
-            <view class="param-meta {{atoItem.isDirty ? 'param-meta--dirty' : ''}}">KP 0x{{atoItem.kpAddress}} {{atoItem.kpWriteValue || '--'}}  KI 0x{{atoItem.kiAddress}} {{atoItem.kiWriteValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input {{atoItem.unit ? 'value-input--with-unit' : ''}}"
-              type="{{atoItem.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{atoItem.inputValue}}"
-              data-index="{{atoItem.sourceIndex}}"
-              data-input-group="ato"
-              bindinput="onAtoBandwidthInput"
-              bindblur="onInputBlur"
-            />
-          </view>
-        </view>
-      </view>
-
-      <view class="panel params-section-panel">
-        <view class="params-section-title">预定位配置</view>
-        <view wx:for="{{prepositionSwitchRegisters}}" wx:key="address" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue}}</view>
-          </view>
-          <switch
-            checked="{{item.value}}"
-            color="#0f766e"
-            disabled="{{!connectedDevice}}"
-            data-index="{{item.sourceIndex}}"
-            bindchange="onTailwindSwitchChange"
-          />
-        </view>
-        <view wx:for="{{prepositionParameterDisplayRegisters}}" wx:key="name" class="param-row input-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input {{item.unit ? 'value-input--with-unit' : ''}}"
-              type="{{item.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{item.inputValue}}"
-              data-index="{{item.sourceIndex}}"
-              data-input-group="preposition"
-              bindinput="onPrepositionParameterInput"
-              bindblur="onInputBlur"
-            />
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'speed'}}">
-      <view class="panel params-section-panel">
-        <view class="params-section-title">速度环路</view>
-        <view wx:for="{{speedLoopInputDisplayRegisters}}" wx:key="name">
-          <view class="param-row input-row">
-            <view class="param-main">
-              <view class="param-name">{{item.name}}</view>
-              <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-            </view>
-            <view class="input-wrap">
-              <input
-                class="value-input {{item.unit ? 'value-input--with-unit' : ''}}"
-                type="{{item.unit ? 'text' : 'digit'}}"
-                placeholder="--"
-                value="{{item.inputValue}}"
-                data-index="{{item.sourceIndex}}"
-                data-input-group="parameter"
-                bindinput="onInputChange"
-                bindblur="onInputBlur"
-              />
-            </view>
-          </view>
-        </view>
-        <view wx:for="{{speedLoopExtraDisplayRegisters}}" wx:key="name">
-          <view class="param-row input-row">
-            <view class="param-main">
-              <view class="param-name">{{item.name}}</view>
-              <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-            </view>
-            <view class="input-wrap">
-              <input
-                class="value-input {{item.unit ? 'value-input--with-unit' : ''}}"
-                type="{{item.unit ? 'text' : 'digit'}}"
-                placeholder="--"
-                value="{{item.inputValue}}"
-                data-index="{{item.sourceIndex}}"
-                data-input-group="speedLoopExtra"
-                bindinput="onSpeedLoopExtraInput"
-                bindblur="onInputBlur"
-              />
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <view class="panel params-section-panel">
-        <view class="params-section-title">VSP曲线</view>
-        <view wx:for="{{vspCurveRegisters}}" wx:key="name">
-          <view class="param-row input-row">
-            <view class="param-main">
-              <view class="param-name">{{item.name}}</view>
-              <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}{{item.actualText ? '  ' + item.actualText : ''}}</view>
-            </view>
-            <view class="input-wrap">
-              <input
-                class="value-input {{item.unit ? 'value-input--with-unit' : ''}}"
-                type="{{item.unit ? 'text' : 'digit'}}"
-                placeholder="--"
-                value="{{item.inputValue}}"
-                data-index="{{item.sourceIndex}}"
-                data-input-group="parameter"
-                bindinput="onInputChange"
-                bindblur="onInputBlur"
-              />
-            </view>
-          </view>
-        </view>
-        <view class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{speedSlopeRegister.name}}</view>
-            <view class="param-meta {{speedSlopeRegister.isDirty ? 'param-meta--dirty' : ''}}">{{speedSlopeRegister.addressDisplay}} {{speedSlopeRegister.writeValue || '--'}}</view>
-          </view>
-          <view class="param-value">{{speedSlopeRegister.writeValue || '--'}}{{speedSlopeRegister.unit ? ' ' + speedSlopeRegister.unit : ''}}</view>
-        </view>
-      </view>
-
-      <view class="panel params-section-panel">
-        <view class="params-section-title">上油参数</view>
-        <view wx:for="{{oilParameterInputRegisters}}" wx:key="name" class="param-row input-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta {{item.isDirty ? 'param-meta--dirty' : ''}}">{{item.addressDisplay}} {{item.writeValue || '--'}}</view>
-          </view>
-          <view class="input-wrap">
-            <input
-              class="value-input {{item.unit ? 'value-input--with-unit' : ''}}"
-              type="{{item.unit ? 'text' : 'digit'}}"
-              placeholder="--"
-              value="{{item.inputValue}}"
-              data-index="{{index}}"
-              data-input-group="oil"
-              bindinput="onOilParameterInput"
-              bindblur="onInputBlur"
-            />
-          </view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:elif="{{activeParamView == 'genericModbus'}}">
-      <view wx:if="{{!genericModbusGroups.length && !genericModbusDialog.visible}}" class="empty-state generic-empty-state">
-        <view class="empty-title">暂无寄存器组</view>
-        <view class="empty-text">点击右上角 + 添加,或从聊天记录导入 JSON</view>
-      </view>
-
+    <block wx:if="{{activeParamView == 'genericModbus'}}">
       <view
         wx:for="{{genericModbusGroups}}"
         wx:for-item="group"
@@ -431,13 +57,15 @@
               data-group-id="{{group.id}}"
               bindtap="openGenericModbusGroup"
             >
-              <view class="panel-icon icon-terminal"></view>
+              <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="openGenericGroupEdit">{{group.name}}</view>
-                <view class="param-meta generic-group-meta">{{group.addressRangeText}} · {{group.quantity}}/{{group.wordQuantity}}{{group.addressWarningText ? ' · ' + group.addressWarningText : ''}}</view>
+                <view class="param-meta generic-group-meta">{{group.addressRangeText}} · {{group.quantity}}/{{group.wordQuantity}}{{group.sourceMetaText ? ' · ' + group.sourceMetaText : ''}}{{group.addressWarningText ? ' · ' + group.addressWarningText : ''}}</view>
               </view>
             </view>
-            <view class="panel-actions generic-group-actions">
+            <view wx:if="{{isGenericProtocol}}" class="panel-actions generic-group-actions">
               <view
                 class="panel-action-button {{connectedDevice && !group.addressOverflow ? '' : 'is-disabled'}}"
                 data-group-id="{{group.id}}"
@@ -464,7 +92,7 @@
     <block wx:elif="{{activeParamView == 'genericModbusGroup'}}">
       <view wx:if="{{activeGenericGroup}}" class="panel generic-group-detail-panel">
         <view class="generic-group-detail-meta">
-          {{activeGenericGroup.addressRangeText}} · {{activeGenericGroup.quantity}}/{{activeGenericGroup.wordQuantity}}{{activeGenericGroup.addressWarningText ? ' · ' + activeGenericGroup.addressWarningText : ''}}
+          {{activeGenericGroup.addressRangeText}} · {{activeGenericGroup.quantity}}/{{activeGenericGroup.wordQuantity}}{{activeGenericGroup.sourceMetaText ? ' · ' + activeGenericGroup.sourceMetaText : ''}}{{activeGenericGroup.addressWarningText ? ' · ' + activeGenericGroup.addressWarningText : ''}}
         </view>
         <view
           wx:for="{{activeGenericRegisterRows.length ? activeGenericRegisterRows : activeGenericGroup.registers}}"
@@ -522,100 +150,6 @@
       </view>
     </block>
 
-    <block wx:elif="{{activeParamView == 'status'}}">
-      <view wx:for="{{statusRegisterGroups}}" wx:for-item="group" wx:key="key" class="panel params-section-panel">
-        <view class="params-section-title">{{group.title}}</view>
-        <view wx:for="{{group.registers}}" wx:for-item="item" wx:key="name" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta">{{item.addressDisplay}} {{item.rawValue}}</view>
-          </view>
-          <view class="param-value">{{item.displayValue}}{{item.displayUnit ? ' ' + item.displayUnit : ''}}</view>
-        </view>
-      </view>
-      <view wx:if="{{userStatusRegisters.length}}" class="panel params-section-panel">
-        <view class="params-section-title">用户状态</view>
-        <view wx:for="{{userStatusRegisters}}" wx:for-item="item" wx:key="name" class="param-row">
-          <view class="param-main">
-            <view class="param-name">{{item.name}}</view>
-            <view class="param-meta">{{item.addressDisplay}} {{item.rawValue}}</view>
-          </view>
-          <view class="param-value">{{item.displayValue}}{{item.displayUnit ? ' ' + item.displayUnit : ''}}</view>
-        </view>
-      </view>
-    </block>
-
-    <block wx:else>
-      <view class="panel panel--collapsed param-entry-panel" data-view="driver" bindtap="openParamView">
-        <view class="panel-header panel-header--with-actions">
-          <view class="panel-heading-toggle">
-            <view class="panel-icon icon-chip"></view>
-            <view class="panel-title">驱动器参数</view>
-          </view>
-          <view class="entry-chevron"></view>
-        </view>
-      </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="estimator" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-bars"></view>
-          <view class="panel-title">估算器参数</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="dq" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-tune"></view>
-          <view class="panel-title">DQ轴电流环参数</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="startup" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-target"></view>
-          <view class="panel-title">启动位置管理</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="speed" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-speed"></view>
-          <view class="panel-title">速度管理</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="protection" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-shield-check"></view>
-          <view class="panel-title">保护</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-
-    <view class="panel panel--collapsed param-entry-panel" data-view="status" bindtap="openParamView">
-      <view class="panel-header panel-header--with-actions">
-        <view class="panel-heading-toggle">
-          <view class="panel-icon icon-status"></view>
-          <view class="panel-title">状态</view>
-        </view>
-        <view class="entry-chevron"></view>
-      </view>
-    </view>
-    </block>
   </view>
 </scroll-view>
 <view wx:if="{{genericModbusDialog.visible}}" class="generic-dialog-mask {{themeClass}}" bindtap="closeGenericModbusDraft">
@@ -711,6 +245,10 @@
             <view class="generic-info-label">地址</view>
             <view class="generic-info-value">{{genericModbusDialog.addressText}}</view>
           </view>
+          <view wx:if="{{genericModbusDialog.sourceMetaText}}" class="generic-info-row">
+            <view class="generic-info-label">来源</view>
+            <view class="generic-info-value">{{genericModbusDialog.sourceMetaText}}</view>
+          </view>
           <view wx:if="{{genericModbusDialog.showDataType}}" class="generic-info-row">
             <view class="generic-info-label">类型</view>
             <view wx:if="{{genericModbusDialog.mode == 'viewRegister'}}" class="generic-info-value">{{genericModbusDialog.dataTypeText}}</view>

+ 60 - 9
pages/settings/settings.js

@@ -1,5 +1,7 @@
 const settingsService = require('../../store/settings-store.js')
 const themeService = require('../../store/theme-store.js')
+const bootloaderService = require('../../features/bootloader/service.js')
+const transport = require('../../transport/ble-core.js')
 const {
   createPageToast
 } = require('../../utils/page-toast.js')
@@ -25,6 +27,7 @@ Page({
     this.crcFileBytes = null
     settingsService.init()
     themeService.init()
+    bootloaderService.init()
 
     this.unsubscribeSettings = settingsService.subscribe((settingsState) => {
       this.setData(getSettingsPageState(settingsState, themeService.getState()))
@@ -32,6 +35,22 @@ Page({
     this.unsubscribeTheme = themeService.subscribe((themeState) => {
       this.setData(getSettingsPageState(settingsService.getState(), themeState))
     })
+    this.unsubscribeTransport = transport.subscribe((transportState) => {
+      this.setData(getSettingsPageState(
+        settingsService.getState(),
+        themeService.getState(),
+        transportState,
+        bootloaderService.getState()
+      ))
+    })
+    this.unsubscribeBootloader = bootloaderService.subscribe((bootloaderState) => {
+      this.setData(getSettingsPageState(
+        settingsService.getState(),
+        themeService.getState(),
+        transport.getState(),
+        bootloaderState
+      ))
+    })
   },
 
   onTabItemTap() {
@@ -67,6 +86,16 @@ Page({
       this.unsubscribeTheme()
       this.unsubscribeTheme = null
     }
+
+    if (this.unsubscribeTransport) {
+      this.unsubscribeTransport()
+      this.unsubscribeTransport = null
+    }
+
+    if (this.unsubscribeBootloader) {
+      this.unsubscribeBootloader()
+      this.unsubscribeBootloader = null
+    }
   },
 
   onNightModeEnabledChange(event) {
@@ -85,7 +114,7 @@ Page({
     const option = this.data.modbusProtocolOptions[Number(event.detail.value)]
     if (!option) return
 
-    settingsService.setModbusProtocolFilter(option.key)
+    settingsService.setModbusProtocolMode(option.key)
   },
 
   onGenericModbusAutoPollChange(event) {
@@ -100,14 +129,6 @@ Page({
     settingsService.setGenericModbusMaxPacketLength(event.detail.value)
   },
 
-  onUserStatusCountBlur(event) {
-    settingsService.setUserStatusCount(event.detail.value, this.data.maxUserStatusCount)
-  },
-
-  onStatusPollIntervalBlur(event) {
-    settingsService.setStatusPollInterval(event.detail.value)
-  },
-
   openToolEntry(event) {
     const view = event.currentTarget.dataset.view
     if (!toolNavigation.isToolView(view)) return
@@ -127,5 +148,35 @@ Page({
     })
   },
 
+  chooseFirmwareFile() {
+    if (this.data.isBootloaderBusy) return
+
+    bootloaderService.chooseFirmwareFile('message')
+  },
+
+  startFirmwareUpgrade() {
+    if (!this.data.connectedDevice || !this.data.isFirmwareReady || this.data.isBootloaderBusy) return
+
+    bootloaderService.startUpgrade()
+  },
+
+  readProgramChecksum() {
+    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
+
+    bootloaderService.readProgramChecksum()
+  },
+
+  handshakeBootloader() {
+    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
+
+    bootloaderService.sendHandshakeKeepAlive()
+  },
+
+  exitBootloader() {
+    if (!this.data.connectedDevice || this.data.isBootloaderBusy) return
+
+    bootloaderService.exitBootloader()
+  },
+
   ...toolPageHandlers
 })

+ 76 - 37
pages/settings/settings.wxml

@@ -13,7 +13,77 @@
 <scroll-view class="scrollarea {{themeClass}} {{activeSettingsView ? 'scrollarea--subpage' : ''}}" scroll-y type="list">
   <view class="page-shell">
     <block wx:if="{{activeSettingsView}}">
-    <block wx:if="{{activeSettingsView == 'crc'}}">
+    <block wx:if="{{activeSettingsView == 'bootloader'}}">
+      <view class="panel params-section-panel upgrade-panel">
+        <view class="panel-header panel-header--with-actions">
+          <view class="panel-icon icon-chip">
+            <image class="panel-icon-image" src="/assets/icons/chip-white.png" mode="aspectFit" />
+          </view>
+          <view class="panel-title">BootLoader</view>
+          <view class="panel-actions upgrade-actions">
+            <view
+              class="panel-action-button {{isBootloaderBusy ? 'is-disabled' : ''}}"
+              bindtap="chooseFirmwareFile"
+            >
+              加载
+            </view>
+            <view
+              class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
+              bindtap="handshakeBootloader"
+            >
+              握手
+            </view>
+            <view
+              class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
+              bindtap="readProgramChecksum"
+            >
+              读取
+            </view>
+            <view
+              class="panel-action-button {{connectedDevice && !isBootloaderBusy ? '' : 'is-disabled'}}"
+              bindtap="exitBootloader"
+            >
+              退出
+            </view>
+            <view
+              class="panel-action-button {{connectedDevice && isFirmwareReady && !isBootloaderBusy ? '' : 'is-disabled'}}"
+              bindtap="startFirmwareUpgrade"
+            >
+              升级
+            </view>
+          </view>
+        </view>
+        <view class="upgrade-body">
+          <view class="upgrade-row">
+            <text class="upgrade-label">芯片型号</text>
+            <text class="upgrade-value">{{chipModel}}</text>
+          </view>
+          <view class="upgrade-row upgrade-row--file">
+            <view class="upgrade-file-head">
+              <text class="upgrade-label">固件文件</text>
+              <view class="upgrade-file-meta">
+                <text class="upgrade-file-meta-item upgrade-file-meta-item--program">{{deviceProgramCrcText}}</text>
+                <text class="upgrade-file-meta-item upgrade-file-meta-item--checksum">{{firmwareChecksumText}}</text>
+                <text class="upgrade-file-meta-item upgrade-file-meta-item--size">{{firmwareSizeText}}</text>
+              </view>
+            </view>
+            <text class="upgrade-file-name">{{firmwareName ? firmwareName : '--'}}</text>
+          </view>
+          <view class="upgrade-row">
+            <text class="upgrade-label">Bootloader</text>
+            <text class="upgrade-value">{{bootloaderVersion}} / {{bootloaderChipId}}</text>
+          </view>
+          <view wx:if="{{bootloaderStatusText || bootloaderDetailText}}" class="upgrade-status {{isFirmwareReady ? 'upgrade-status--ready' : ''}}">
+            {{bootloaderStatusText}}<text wx:if="{{bootloaderDetailText}}"> · {{bootloaderDetailText}}</text>
+          </view>
+          <view class="upgrade-progress">
+            <view class="upgrade-progress-bar" style="width: {{bootloaderProgress}}%;"></view>
+          </view>
+        </view>
+      </view>
+    </block>
+
+    <block wx:elif="{{activeSettingsView == 'crc'}}">
       <view class="panel params-section-panel crc-algorithm-panel {{crcAlgorithmCollapsed ? 'panel--collapsed' : ''}}">
         <view class="param-row input-row">
           <picker
@@ -628,8 +698,8 @@
       <view class="params-section-title">Modbus</view>
       <view class="settings-row">
         <view class="settings-row-main">
-          <view class="param-name">协议筛选</view>
-          <view class="param-meta">用于选择当前调试协议</view>
+          <view class="param-name">协议模式</view>
+          <view class="param-meta">{{isPrivateProtocol ? '0x40 / 0x41 / 0x42' : '标准功能码寄存器'}}</view>
         </view>
         <picker
           mode="selector"
@@ -656,38 +726,7 @@
           />
         </view>
       </view>
-      <view wx:if="{{!isGenericProtocol}}" class="settings-row settings-row--input">
-        <view class="settings-row-main">
-          <view class="param-name">用户状态个数</view>
-          <view class="param-meta">0 - {{maxUserStatusCount}}</view>
-        </view>
-        <view class="settings-input-wrap">
-          <input
-            class="value-input settings-value-input"
-            type="number"
-            value="{{userStatusCount}}"
-            bindblur="onUserStatusCountBlur"
-            bindconfirm="onUserStatusCountBlur"
-          />
-        </view>
-      </view>
-      <view wx:if="{{!isGenericProtocol}}" class="settings-row settings-row--input">
-        <view class="settings-row-main">
-          <view class="param-name">状态轮询间隔</view>
-          <view class="param-meta">{{statusPollMinInterval}} - {{statusPollMaxInterval}} ms</view>
-        </view>
-        <view class="settings-input-wrap settings-input-wrap--unit">
-          <input
-            class="value-input settings-value-input settings-value-input--unit"
-            type="number"
-            value="{{statusPollInterval}}"
-            bindblur="onStatusPollIntervalBlur"
-            bindconfirm="onStatusPollIntervalBlur"
-          />
-          <text class="settings-unit settings-unit--inside">ms</text>
-        </view>
-      </view>
-      <view wx:if="{{isGenericProtocol}}" class="settings-row">
+      <view class="settings-row">
         <view class="settings-row-main">
           <view class="param-name">自动轮询</view>
           <view class="param-meta">{{genericModbusAutoPollEnabled ? '已启用' : '已停止'}}</view>
@@ -698,7 +737,7 @@
           bindchange="onGenericModbusAutoPollChange"
         />
       </view>
-      <view wx:if="{{isGenericProtocol}}" class="settings-row settings-row--input">
+      <view class="settings-row settings-row--input">
         <view class="settings-row-main">
           <view class="param-name">轮询间隔</view>
           <view class="param-meta">{{statusPollMinInterval}} - {{statusPollMaxInterval}} ms</view>
@@ -714,7 +753,7 @@
           <text class="settings-unit settings-unit--inside">ms</text>
         </view>
       </view>
-      <view wx:if="{{isGenericProtocol}}" class="settings-row settings-row--input">
+      <view class="settings-row settings-row--input">
         <view class="settings-row-main">
           <view class="param-name">最大包长</view>
           <view class="param-meta">0 为无限制,最小 {{genericModbusMinPacketLength}} 字节</view>

+ 149 - 0
pages/settings/settings.wxss

@@ -69,6 +69,155 @@
   text-overflow: ellipsis;
 }
 
+.upgrade-actions {
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  max-width: 286rpx;
+  row-gap: 8rpx;
+}
+
+.upgrade-body {
+  padding: 8rpx 24rpx 24rpx;
+}
+
+.upgrade-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20rpx;
+  min-height: 62rpx;
+  border-top: 1rpx solid #edf2f7;
+}
+
+.upgrade-row:first-child {
+  border-top: 0;
+}
+
+.upgrade-row--file {
+  display: block;
+  min-height: 0;
+  padding-top: 14rpx;
+  padding-bottom: 14rpx;
+}
+
+.upgrade-file-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 14rpx;
+}
+
+.upgrade-file-meta {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10rpx;
+  min-width: 0;
+  flex: 1;
+  overflow: hidden;
+}
+
+.upgrade-file-meta-item {
+  flex: none;
+  min-width: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.upgrade-file-meta-item--program {
+  color: var(--accent-dark);
+  font-weight: 800;
+}
+
+.upgrade-file-meta-item--checksum,
+.upgrade-file-meta-item--size {
+  color: #64748b;
+}
+
+.upgrade-file-name {
+  display: block;
+  margin-top: 8rpx;
+  color: #111827;
+  font-size: 24rpx;
+  line-height: 1.35;
+  font-weight: 700;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.upgrade-label {
+  flex: none;
+  color: #64748b;
+  font-size: 24rpx;
+  line-height: 1.35;
+}
+
+.upgrade-value {
+  min-width: 0;
+  flex: 1;
+  color: #111827;
+  font-size: 24rpx;
+  line-height: 1.35;
+  text-align: right;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.upgrade-status {
+  margin-top: 14rpx;
+  padding: 14rpx 16rpx;
+  border-radius: 12rpx;
+  background: #f8fafc;
+  color: #64748b;
+  font-size: 23rpx;
+  line-height: 1.45;
+  font-weight: 700;
+  word-break: break-all;
+}
+
+.upgrade-status--ready {
+  background: #effaf8;
+  color: var(--accent-dark);
+}
+
+.upgrade-progress {
+  position: relative;
+  height: 14rpx;
+  margin-top: 16rpx;
+  border-radius: 999rpx;
+  background: #e5e7eb;
+  overflow: hidden;
+}
+
+.upgrade-progress-bar {
+  height: 100%;
+  min-width: 0;
+  border-radius: inherit;
+  background: linear-gradient(90deg, #10b981 0%, #0f8f87 100%);
+  transition: width 0.18s ease;
+}
+
+.theme-dark .upgrade-file-meta-item--checksum,
+.theme-dark .upgrade-file-meta-item--size,
+.theme-dark .upgrade-file-name {
+  color: #cbd5e1;
+}
+
+.theme-dark .upgrade-file-meta-item--program {
+  color: #5eead4;
+}
+
+.theme-dark .upgrade-progress {
+  background: #263241;
+}
+
+.theme-dark .upgrade-progress-bar {
+  background: linear-gradient(90deg, #2dd4bf 0%, #14b8a6 100%);
+}
+
 .settings-input-wrap {
   flex: none;
   display: flex;

+ 191 - 3
protocols/modbus-rtu/client.js

@@ -1,8 +1,13 @@
 const {
+  buildCodeInfoQueryFrame,
+  buildDebugReadMemoryFrame,
+  buildDebugWriteMemoryFrame,
   buildReadFrame,
   buildWriteMultipleRegistersFrame,
   buildWriteSingleCoilFrame,
   buildWriteSingleRegisterFrame,
+  getMaxDebugReadByteLength,
+  getMaxDebugWriteByteLength,
   getMaxReadQuantity,
   getMaxWriteMultipleRegisterQuantity
 } = require('./frame.js')
@@ -32,6 +37,14 @@ function getChunkLabel(label, chunks, chunk) {
   return `${label} ${formatAddress(chunk.address)}-${formatAddress(chunk.address + chunk.quantity - 1)}`
 }
 
+function isBroadcastAddress(slaveAddress) {
+  return Number(slaveAddress) === 0
+}
+
+function showBroadcastReadAlert(label) {
+  transport.showCommandAlert(label || '读取命令错误', '广播地址 0x00 不支持读取')
+}
+
 function splitQuantity(startAddress, quantity, maxQuantity) {
   const chunks = []
   let address = Number(startAddress) || 0
@@ -57,7 +70,35 @@ function getReadChunks(functionCode, startAddress, quantity, options = {}) {
   return splitQuantity(startAddress, quantity, maxQuantity || quantity)
 }
 
+function getDebugReadChunks(startAddress, byteLength, options = {}) {
+  const maxByteLength = getMaxDebugReadByteLength(options.maxFrameBytes)
+
+  return splitQuantity(startAddress, byteLength, maxByteLength || byteLength)
+}
+
+function getDebugWriteChunks(startAddress, bytes, options = {}) {
+  const sourceBytes = Array.prototype.slice.call(bytes || [])
+  const maxByteLength = getMaxDebugWriteByteLength(options.maxFrameBytes)
+  const chunks = splitQuantity(startAddress, sourceBytes.length, maxByteLength || sourceBytes.length)
+  let offset = 0
+
+  return chunks.map((chunk) => {
+    const dataBytes = sourceBytes.slice(offset, offset + chunk.quantity)
+    offset += chunk.quantity
+
+    return {
+      ...chunk,
+      dataBytes
+    }
+  })
+}
+
 async function sendReadChunk(slaveAddress, functionCode, chunk, label, kind, options = {}) {
+  if (isBroadcastAddress(slaveAddress)) {
+    showBroadcastReadAlert(label)
+    return false
+  }
+
   return transport.sendManagedFrame(
     buildReadFrame(slaveAddress, functionCode, chunk.address, chunk.quantity, {
       maxFrameBytes: options.maxFrameBytes
@@ -77,6 +118,32 @@ async function sendReadChunk(slaveAddress, functionCode, chunk, label, kind, opt
   )
 }
 
+async function sendDebugReadChunk(slaveAddress, memoryType, chunk, label, kind, options = {}) {
+  if (isBroadcastAddress(slaveAddress)) {
+    showBroadcastReadAlert(label)
+    return false
+  }
+
+  return transport.sendManagedFrame(
+    buildDebugReadMemoryFrame(slaveAddress, memoryType, chunk.address, chunk.quantity, {
+      maxFrameBytes: options.maxFrameBytes
+    }),
+    label,
+    {
+      address: chunk.address,
+      functionCode: 0x41,
+      kind,
+      memoryType,
+      quantity: chunk.quantity,
+      slaveAddress
+    },
+    {
+      maxFrameBytes: options.maxFrameBytes,
+      showModal: options.showModal
+    }
+  )
+}
+
 async function readSpans(slaveAddress, functionCode, spans, label, kind, options = {}) {
   const readValues = {
     coils: {},
@@ -141,6 +208,88 @@ async function readRegisterWords(slaveAddress, functionCode, startAddress, quant
   return words
 }
 
+async function readDebugMemory(slaveAddress, memoryType, startAddress, byteLength, label, kind = 'debug-memory-read', options = {}) {
+  const bytes = []
+  const chunks = getDebugReadChunks(startAddress, byteLength, options)
+
+  for (const chunk of chunks) {
+    const response = await sendDebugReadChunk(
+      slaveAddress,
+      memoryType,
+      chunk,
+      getChunkLabel(label, chunks, chunk),
+      kind,
+      options
+    )
+    if (!response) return null
+
+    const dataBytes = Array.isArray(response.dataBytes) ? response.dataBytes : []
+    dataBytes.forEach((byte, index) => {
+      bytes[chunk.address - startAddress + index] = Number(byte) & 0xFF
+    })
+
+    if (typeof options.onChunk === 'function') {
+      options.onChunk(response, chunk)
+    }
+  }
+
+  return bytes
+}
+
+async function queryCodeInfoBlock(slaveAddress, label = '查询Code信息块', kind = 'code-info-query', options = {}) {
+  if (isBroadcastAddress(slaveAddress)) {
+    showBroadcastReadAlert(label)
+    return null
+  }
+
+  const response = await transport.sendManagedFrame(
+    buildCodeInfoQueryFrame(slaveAddress),
+    label,
+    {
+      functionCode: 0x40,
+      kind,
+      quantity: 1,
+      slaveAddress
+    },
+    {
+      maxFrameBytes: options.maxFrameBytes,
+      showModal: options.showModal
+    }
+  )
+  if (!response) return null
+
+  return {
+    address: response.codeInfoAddress,
+    byteLength: response.codeInfoLength,
+    memoryType: response.memoryType
+  }
+}
+
+async function readCodeInfoBlock(slaveAddress, label = '读取Code信息块', kind = 'code-info-read', options = {}) {
+  const info = await queryCodeInfoBlock(slaveAddress, label, 'code-info-query', options)
+  if (!info) return null
+  if (info.memoryType !== 0x03) {
+    transport.showCommandAlert(label, '设备返回的信息块不在 code 区')
+    return null
+  }
+
+  const bytes = await readDebugMemory(
+    slaveAddress,
+    0x03,
+    info.address,
+    info.byteLength,
+    label,
+    kind,
+    options
+  )
+  if (!bytes) return null
+
+  return {
+    ...info,
+    bytes
+  }
+}
+
 async function readBitValues(slaveAddress, functionCode, startAddress, quantity, label, kind, options = {}) {
   const result = await readSpans(
     slaveAddress,
@@ -166,7 +315,7 @@ function writeSingleCoil(slaveAddress, address, checked, label, kind = 'coil-wri
   return transport.sendManagedFrame(
     buildWriteSingleCoilFrame(slaveAddress, address, checked),
     label,
-    {
+    isBroadcastAddress(slaveAddress) ? null : {
       address,
       functionCode: 0x05,
       kind,
@@ -185,7 +334,7 @@ function writeSingleRegister(slaveAddress, address, value, label, kind = 'regist
   return transport.sendManagedFrame(
     buildWriteSingleRegisterFrame(slaveAddress, address, value),
     label,
-    {
+    isBroadcastAddress(slaveAddress) ? null : {
       address,
       functionCode: 0x06,
       kind,
@@ -206,7 +355,7 @@ function writeMultipleRegisters(slaveAddress, address, values, label, kind = 're
       maxFrameBytes: options.maxFrameBytes
     }),
     label,
-    {
+    isBroadcastAddress(slaveAddress) ? null : {
       address,
       functionCode: 0x10,
       kind,
@@ -220,16 +369,55 @@ function writeMultipleRegisters(slaveAddress, address, values, label, kind = 're
   )
 }
 
+async function writeDebugMemory(slaveAddress, memoryType, startAddress, bytes, label, kind = 'debug-memory-write', options = {}) {
+  const dataBytes = Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
+  const chunks = getDebugWriteChunks(startAddress, dataBytes, options)
+
+  for (const chunk of chunks) {
+    const response = await transport.sendManagedFrame(
+      buildDebugWriteMemoryFrame(slaveAddress, memoryType, chunk.address, chunk.dataBytes, {
+        maxFrameBytes: options.maxFrameBytes
+      }),
+      getChunkLabel(label, chunks, chunk),
+      isBroadcastAddress(slaveAddress) ? null : {
+        address: chunk.address,
+        functionCode: 0x42,
+        kind,
+        memoryType,
+        quantity: chunk.quantity,
+        slaveAddress
+      },
+      {
+        maxFrameBytes: options.maxFrameBytes,
+        showModal: options.showModal
+      }
+    )
+    if (!response) return false
+
+    if (typeof options.onChunk === 'function') {
+      options.onChunk(response, chunk)
+    }
+  }
+
+  return true
+}
+
 module.exports = {
+  getDebugReadChunks,
+  getDebugWriteChunks,
   getReadChunks,
   getSharedSlaveAddress,
   getMaxReadQuantity,
+  queryCodeInfoBlock,
   readBitValues,
+  readCodeInfoBlock,
+  readDebugMemory,
   readRegisterWords,
   readSingleHoldingWord,
   readSpans,
   splitQuantity,
   getMaxWriteMultipleRegisterQuantity,
+  writeDebugMemory,
   writeMultipleRegisters,
   writeSingleCoil,
   writeSingleRegister

+ 101 - 0
protocols/modbus-rtu/frame.js

@@ -10,6 +10,12 @@ const MODBUS_CRC_OPTIONS = {
 const MAX_MODBUS_DMA_BYTES = 64
 const MODBUS_READ_RESPONSE_OVERHEAD = 5
 const MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD = 9
+const MODBUS_CODE_INFO_FUNCTION_CODE = 0x40
+const MODBUS_DEBUG_READ_FUNCTION_CODE = 0x41
+const MODBUS_DEBUG_WRITE_FUNCTION_CODE = 0x42
+const MODBUS_CODE_INFO_RESPONSE_LENGTH = 9
+const MODBUS_DEBUG_READ_RESPONSE_OVERHEAD = 6
+const MODBUS_DEBUG_WRITE_REQUEST_OVERHEAD = 9
 const MAX_READ_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) / 2)
 const MAX_READ_COIL_QUANTITY = (MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) * 8
 const MAX_WRITE_MULTIPLE_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2)
@@ -31,6 +37,27 @@ function toWord(value, label) {
   return value
 }
 
+function toMemoryType(value) {
+  const memoryType = toByte(value, '内存区域')
+  if (memoryType > 0x03) {
+    throw new Error('内存区域必须为 data/idata/xdata/code')
+  }
+
+  return memoryType
+}
+
+function toDebugByteLength(value, label = '字节长度') {
+  const byteLength = toWord(value, label)
+  if (byteLength === 0) {
+    throw new Error(`${label}必须大于 0`)
+  }
+  if (byteLength > 0xFF) {
+    throw new Error('单帧字节长度不能超过 255')
+  }
+
+  return byteLength
+}
+
 function splitWord(value) {
   return [(value >> 8) & 0xFF, value & 0xFF]
 }
@@ -62,6 +89,20 @@ function getMaxWriteMultipleRegisterQuantity(maxFrameBytes = MAX_MODBUS_DMA_BYTE
   return Math.max(0, Math.floor((frameBytes - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2))
 }
 
+function getMaxDebugReadByteLength(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
+  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
+  if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFF
+
+  return Math.max(0, Math.min(0xFF, frameBytes - MODBUS_DEBUG_READ_RESPONSE_OVERHEAD))
+}
+
+function getMaxDebugWriteByteLength(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
+  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
+  if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFF
+
+  return Math.max(0, Math.min(0xFF, frameBytes - MODBUS_DEBUG_WRITE_REQUEST_OVERHEAD))
+}
+
 function buildReadFrame(slaveAddress, functionCode, address, quantity, options = {}) {
   const slave = toByte(slaveAddress, '从站地址')
   const command = toByte(functionCode, '功能码')
@@ -133,6 +174,54 @@ function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options
   )
 }
 
+function buildCodeInfoQueryFrame(slaveAddress) {
+  const slave = toByte(slaveAddress, '从站地址')
+
+  return appendCrc16Modbus(
+    [slave, MODBUS_CODE_INFO_FUNCTION_CODE],
+    MODBUS_CRC_OPTIONS
+  )
+}
+
+function buildDebugReadMemoryFrame(slaveAddress, memoryType, address, byteLength, options = {}) {
+  const slave = toByte(slaveAddress, '从站地址')
+  const memType = toMemoryType(memoryType)
+  const startAddress = toWord(address, '内存地址')
+  const length = toDebugByteLength(byteLength)
+  const maxByteLength = getMaxDebugReadByteLength(options.maxFrameBytes)
+
+  if (maxByteLength > 0 && length > maxByteLength) {
+    throw new Error(`单帧最多读取 ${maxByteLength} 字节`)
+  }
+
+  return appendCrc16Modbus(
+    [slave, MODBUS_DEBUG_READ_FUNCTION_CODE, memType].concat(splitWord(startAddress), splitWord(length)),
+    MODBUS_CRC_OPTIONS
+  )
+}
+
+function buildDebugWriteMemoryFrame(slaveAddress, memoryType, address, bytes, options = {}) {
+  const slave = toByte(slaveAddress, '从站地址')
+  const memType = toMemoryType(memoryType)
+  const startAddress = toWord(address, '内存地址')
+  const dataBytes = Array.prototype.slice.call(bytes || []).map((byte) => toByte(Number(byte), '写入字节'))
+  const length = toDebugByteLength(dataBytes.length)
+  const maxByteLength = getMaxDebugWriteByteLength(options.maxFrameBytes)
+
+  if (memType === 0x03) {
+    throw new Error('code 区暂不支持写入')
+  }
+  if (maxByteLength > 0 && length > maxByteLength) {
+    throw new Error(`单帧最多写入 ${maxByteLength} 字节`)
+  }
+
+  return appendCrc16Modbus(
+    [slave, MODBUS_DEBUG_WRITE_FUNCTION_CODE, memType]
+      .concat(splitWord(startAddress), splitWord(length), dataBytes),
+    MODBUS_CRC_OPTIONS
+  )
+}
+
 function formatHex(bytes) {
   return bytes.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
 }
@@ -140,6 +229,9 @@ function formatHex(bytes) {
 function getReadResponseByteLength(functionCode, quantity) {
   if (functionCode === 0x01 || functionCode === 0x02) return MODBUS_READ_RESPONSE_OVERHEAD + Math.ceil(Number(quantity || 0) / 8)
   if (functionCode === 0x03 || functionCode === 0x04) return MODBUS_READ_RESPONSE_OVERHEAD + Number(quantity || 0) * 2
+  if (functionCode === MODBUS_CODE_INFO_FUNCTION_CODE) return MODBUS_CODE_INFO_RESPONSE_LENGTH
+  if (functionCode === MODBUS_DEBUG_READ_FUNCTION_CODE) return MODBUS_DEBUG_READ_RESPONSE_OVERHEAD + Number(quantity || 0)
+  if (functionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) return 9
 
   return 0
 }
@@ -150,12 +242,21 @@ module.exports = {
   MAX_READ_REGISTER_QUANTITY,
   MAX_WRITE_MULTIPLE_REGISTER_QUANTITY,
   MODBUS_CRC_OPTIONS,
+  MODBUS_CODE_INFO_FUNCTION_CODE,
+  MODBUS_CODE_INFO_RESPONSE_LENGTH,
+  MODBUS_DEBUG_READ_FUNCTION_CODE,
+  MODBUS_DEBUG_WRITE_FUNCTION_CODE,
   UNLIMITED_FRAME_BYTES,
+  buildCodeInfoQueryFrame,
+  buildDebugReadMemoryFrame,
+  buildDebugWriteMemoryFrame,
   buildReadFrame,
   buildWriteMultipleRegistersFrame,
   buildWriteSingleCoilFrame,
   buildWriteSingleRegisterFrame,
   formatHex,
+  getMaxDebugReadByteLength,
+  getMaxDebugWriteByteLength,
   getMaxWriteMultipleRegisterQuantity,
   getMaxReadQuantity,
   getReadResponseByteLength,

+ 116 - 1
protocols/modbus-rtu/response.js

@@ -1,6 +1,10 @@
 const {
   MAX_MODBUS_DMA_BYTES,
+  MODBUS_CODE_INFO_FUNCTION_CODE,
+  MODBUS_CODE_INFO_RESPONSE_LENGTH,
   MODBUS_CRC_OPTIONS,
+  MODBUS_DEBUG_READ_FUNCTION_CODE,
+  MODBUS_DEBUG_WRITE_FUNCTION_CODE,
   getReadResponseByteLength,
   hasValidCrc16Modbus
 } = require('./frame.js')
@@ -77,6 +81,52 @@ function parseModbusResponse(bytes) {
     }
   }
 
+  if (functionCode === MODBUS_CODE_INFO_FUNCTION_CODE) {
+    if (bytes.length < MODBUS_CODE_INFO_RESPONSE_LENGTH) return null
+
+    return {
+      address: ((bytes[3] << 8) | bytes[4]) & 0xFFFF,
+      byteLength: ((bytes[5] << 8) | bytes[6]) & 0xFFFF,
+      codeInfoAddress: ((bytes[3] << 8) | bytes[4]) & 0xFFFF,
+      codeInfoLength: ((bytes[5] << 8) | bytes[6]) & 0xFFFF,
+      functionCode,
+      isException: false,
+      memoryType: bytes[2],
+      slaveAddress
+    }
+  }
+
+  if (functionCode === MODBUS_DEBUG_READ_FUNCTION_CODE) {
+    const memoryType = bytes[2]
+    const byteCount = bytes[3]
+    const dataEnd = 4 + byteCount
+    if (bytes.length < dataEnd + 2) return null
+
+    return {
+      byteCount,
+      dataBytes: bytes.slice(4, dataEnd),
+      functionCode,
+      isException: false,
+      memoryType,
+      slaveAddress,
+      words: bytesToWords(bytes.slice(4, dataEnd).concat(byteCount % 2 ? [0] : []))
+    }
+  }
+
+  if (functionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) {
+    if (bytes.length < 9) return null
+
+    return {
+      address: ((bytes[3] << 8) | bytes[4]) & 0xFFFF,
+      byteLength: ((bytes[5] << 8) | bytes[6]) & 0xFFFF,
+      functionCode,
+      isException: false,
+      memoryType: bytes[2],
+      quantity: ((bytes[5] << 8) | bytes[6]) & 0xFFFF,
+      slaveAddress
+    }
+  }
+
   if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
     return {
       address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
@@ -95,10 +145,44 @@ function parseModbusResponse(bytes) {
 }
 
 function parseModbusRequest(bytes) {
-  if (!Array.isArray(bytes) || bytes.length < 6 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
+  if (!Array.isArray(bytes) || bytes.length < 4 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
 
   const slaveAddress = bytes[0]
   const functionCode = bytes[1]
+  if (functionCode === MODBUS_CODE_INFO_FUNCTION_CODE) {
+    return {
+      functionCode,
+      kind: 'raw-hex',
+      quantity: 1,
+      slaveAddress
+    }
+  }
+
+  if (bytes.length < 6) return null
+
+  if (functionCode === MODBUS_DEBUG_READ_FUNCTION_CODE || functionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) {
+    if (bytes.length < 9) return null
+
+    const memoryType = bytes[2]
+    const address = ((bytes[3] << 8) | bytes[4]) & 0xFFFF
+    const quantity = ((bytes[5] << 8) | bytes[6]) & 0xFFFF
+    const request = {
+      address,
+      functionCode,
+      kind: 'raw-hex',
+      memoryType,
+      quantity,
+      slaveAddress
+    }
+
+    if (functionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) {
+      if (bytes.length < 7 + quantity + 2) return null
+      request.dataBytes = bytes.slice(7, 7 + quantity)
+    }
+
+    return request
+  }
+
   const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
   let quantity = 1
   let value
@@ -139,6 +223,20 @@ function getExpectedResponseLength(expected, responseFunctionCode, responseBytes
     return 3 + Number(responseBytes[2] || 0) + 2
   }
 
+  if (responseFunctionCode === MODBUS_CODE_INFO_FUNCTION_CODE) {
+    return MODBUS_CODE_INFO_RESPONSE_LENGTH
+  }
+
+  if (responseFunctionCode === MODBUS_DEBUG_READ_FUNCTION_CODE) {
+    if (responseBytes.length < 4) return 0
+
+    return 4 + Number(responseBytes[3] || 0) + 2
+  }
+
+  if (responseFunctionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) {
+    return 9
+  }
+
   if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
     return 8
   }
@@ -155,6 +253,23 @@ function isExpectedResponse(response, expected) {
     return Array.isArray(response.words) && response.words.length >= expected.quantity
   }
 
+  if (response.functionCode === MODBUS_CODE_INFO_FUNCTION_CODE) {
+    return response.memoryType === 0x03
+      && response.codeInfoLength > 0
+  }
+
+  if (response.functionCode === MODBUS_DEBUG_READ_FUNCTION_CODE) {
+    return response.memoryType === expected.memoryType
+      && Array.isArray(response.dataBytes)
+      && response.dataBytes.length === expected.quantity
+  }
+
+  if (response.functionCode === MODBUS_DEBUG_WRITE_FUNCTION_CODE) {
+    return response.memoryType === expected.memoryType
+      && response.address === expected.address
+      && response.byteLength === expected.quantity
+  }
+
   if (response.functionCode === 0x10) {
     return response.address === expected.address && response.quantityOrValue === expected.quantity
   }

+ 6 - 1
protocols/transport-helpers.js

@@ -30,7 +30,12 @@ function inspectReceivedBytes(rawBytes, context = {}) {
 }
 
 function parseSendExpected(bytes) {
-  return parseModbusRequest(bytes)
+  const expected = parseModbusRequest(bytes)
+  if (!expected) return expected
+  if (expected.slaveAddress === 0x00 && [0x05, 0x06, 0x10, 0x42].includes(expected.functionCode)) return null
+  if (expected.slaveAddress === 0x00 && [0x01, 0x02, 0x03, 0x04, 0x40, 0x41].includes(expected.functionCode)) return null
+
+  return expected
 }
 
 module.exports = {

+ 15 - 46
store/settings-store.js

@@ -10,19 +10,17 @@ const {
 
 const STORAGE_KEY = 'app-settings'
 const MODBUS_PROTOCOL_OPTIONS = [
-  { key: 'motor-control', label: '电机控制协议' },
+  { key: 'private', label: '私有协议' },
   { key: 'generic', label: '通用协议' }
 ]
 const DEFAULT_SETTINGS = {
   genericModbusAutoPollEnabled: false,
   genericModbusMaxPacketLength: 64,
   genericModbusPollInterval: 100,
+  modbusProtocolMode: 'private',
   modbusSlaveAddress: 'F0',
-  modbusProtocolFilter: MODBUS_PROTOCOL_OPTIONS[0].key,
   nightModeEnabled: false,
-  nightModeFollowSystem: true,
-  statusPollInterval: 100,
-  userStatusCount: 0
+  nightModeFollowSystem: true
 }
 const STATUS_POLL_MIN_INTERVAL = 100
 const STATUS_POLL_MAX_INTERVAL = 3000
@@ -55,11 +53,11 @@ function normalizeGenericPacketLength(value, fallback = DEFAULT_SETTINGS.generic
   return Math.max(rounded, GENERIC_MODBUS_MIN_PACKET_LENGTH)
 }
 
-function normalizeModbusProtocolFilter(value) {
+function normalizeModbusProtocolMode(value) {
   const key = String(value || '').trim()
-  const matchedOption = MODBUS_PROTOCOL_OPTIONS.find((option) => option.key === key)
+  const matched = MODBUS_PROTOCOL_OPTIONS.find((option) => option.key === key)
 
-  return matchedOption ? matchedOption.key : DEFAULT_SETTINGS.modbusProtocolFilter
+  return matched ? matched.key : DEFAULT_SETTINGS.modbusProtocolMode
 }
 
 function parseHexByte(value, label = '从机地址') {
@@ -86,17 +84,10 @@ function normalizeSettings(settings = {}) {
       STATUS_POLL_MAX_INTERVAL,
       DEFAULT_SETTINGS.genericModbusPollInterval
     ),
+    modbusProtocolMode: normalizeModbusProtocolMode(settings.modbusProtocolMode),
     modbusSlaveAddress: normalizeHexByte(settings.modbusSlaveAddress),
-    modbusProtocolFilter: normalizeModbusProtocolFilter(settings.modbusProtocolFilter),
     nightModeEnabled: !!settings.nightModeEnabled,
-    nightModeFollowSystem: settings.nightModeFollowSystem !== false,
-    statusPollInterval: clampInteger(
-      settings.statusPollInterval,
-      STATUS_POLL_MIN_INTERVAL,
-      STATUS_POLL_MAX_INTERVAL,
-      DEFAULT_SETTINGS.statusPollInterval
-    ),
-    userStatusCount: clampInteger(settings.userStatusCount, 0, 999, DEFAULT_SETTINGS.userStatusCount)
+    nightModeFollowSystem: settings.nightModeFollowSystem !== false
   }
 }
 
@@ -191,16 +182,16 @@ function setModbusSlaveAddress(value) {
   })
 }
 
-function getModbusSlaveAddress() {
+function setModbusProtocolMode(value) {
   init()
-  return parseHexByte(state.modbusSlaveAddress, 'Modbus从机地址')
+  setState({
+    modbusProtocolMode: normalizeModbusProtocolMode(value)
+  })
 }
 
-function setModbusProtocolFilter(value) {
+function getModbusSlaveAddress() {
   init()
-  setState({
-    modbusProtocolFilter: normalizeModbusProtocolFilter(value)
-  })
+  return parseHexByte(state.modbusSlaveAddress, 'Modbus从机地址')
 }
 
 function setGenericModbusAutoPollEnabled(value) {
@@ -224,42 +215,20 @@ function setGenericModbusPollInterval(value) {
   })
 }
 
-function getModbusProtocolFilter() {
-  init()
-  return state.modbusProtocolFilter
-}
-
-function setStatusPollInterval(value) {
-  init()
-  setState({
-    statusPollInterval: value
-  })
-}
-
-function setUserStatusCount(value, maxValue = 999) {
-  init()
-  setState({
-    userStatusCount: clampInteger(value, 0, maxValue, state.userStatusCount)
-  })
-}
-
 module.exports = {
   GENERIC_MODBUS_MIN_PACKET_LENGTH,
   MODBUS_PROTOCOL_OPTIONS,
   STATUS_POLL_MAX_INTERVAL,
   STATUS_POLL_MIN_INTERVAL,
-  getModbusProtocolFilter,
   getModbusSlaveAddress,
   getState,
   init,
   setGenericModbusAutoPollEnabled,
   setGenericModbusMaxPacketLength,
   setGenericModbusPollInterval,
+  setModbusProtocolMode,
   setModbusSlaveAddress,
-  setModbusProtocolFilter,
   setNightModeEnabled,
   setNightModeFollowSystem,
-  setStatusPollInterval,
-  setUserStatusCount,
   subscribe
 }

+ 0 - 8
store/theme-store.js

@@ -12,14 +12,6 @@ const TAB_ITEMS = [
     darkIconPath: 'assets/tab/home-dark.png',
     darkSelectedIconPath: 'assets/tab/home-active-dark.png'
   },
-  {
-    pagePath: 'pages/index/index',
-    text: '控制',
-    iconPath: 'assets/tab/control.png',
-    selectedIconPath: 'assets/tab/control-active.png',
-    darkIconPath: 'assets/tab/control-dark.png',
-    darkSelectedIconPath: 'assets/tab/control-active-dark.png'
-  },
   {
     pagePath: 'pages/params/params',
     text: '参数',

Vissa filer visades inte eftersom för många filer har ändrats