code-info-parser.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. const {
  2. bytesToHex,
  3. bytesToUtf8Text,
  4. trimTrailingNullBytes
  5. } = require('../../utils/binary-utils.js')
  6. const FIXED_HEADER_BYTE_LENGTH = 44
  7. const STRUCT_ENTRY_MIN_BYTE_LENGTH = 5
  8. const MEMORY_TYPE_AREAS = {
  9. 0x01: 'DATA',
  10. 0x02: 'IDATA',
  11. 0x03: 'XDATA',
  12. 0x04: 'CODE'
  13. }
  14. function toBytes(bytes) {
  15. return Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
  16. }
  17. function readUint16(bytes, offset) {
  18. if (offset + 1 >= bytes.length) return 0
  19. return (((bytes[offset] || 0) << 8) | (bytes[offset + 1] || 0)) & 0xFFFF
  20. }
  21. function readFloat(bytes, offset) {
  22. if (offset + 3 >= bytes.length) return 0
  23. const buffer = new ArrayBuffer(4)
  24. const view = new DataView(buffer)
  25. for (let index = 0; index < 4; index += 1) {
  26. view.setUint8(index, bytes[offset + index] || 0)
  27. }
  28. return view.getFloat32(0, false)
  29. }
  30. function readAscii(bytes, offset, byteLength) {
  31. const source = trimTrailingNullBytes(bytes.slice(offset, offset + byteLength))
  32. return source
  33. .map((byte) => (byte >= 0x20 && byte <= 0x7E ? String.fromCharCode(byte) : ''))
  34. .join('')
  35. .trim()
  36. }
  37. function readUtf8OrAscii(bytes, offset, byteLength) {
  38. return bytesToUtf8Text(bytes.slice(offset, offset + byteLength)).trim()
  39. }
  40. function formatAddress(address) {
  41. return `0x${Number(address || 0).toString(16).toUpperCase().padStart(4, '0')}`
  42. }
  43. function normalizeTypeName(value, fallback) {
  44. const text = String(value || '').trim()
  45. return text || fallback
  46. }
  47. function parseStructEntry(bytes, offset, entryLength, index) {
  48. const byteAddr = readUint16(bytes, offset)
  49. const byteLength = readUint16(bytes, offset + 2)
  50. const memType = bytes[offset + 4] & 0xFF
  51. const nameByteLength = Math.max(0, entryLength - STRUCT_ENTRY_MIN_BYTE_LENGTH)
  52. const typeName = normalizeTypeName(
  53. readAscii(bytes, offset + STRUCT_ENTRY_MIN_BYTE_LENGTH, nameByteLength),
  54. `struct_${index + 1}`
  55. )
  56. const memoryArea = MEMORY_TYPE_AREAS[memType] || 'UNKNOWN'
  57. return {
  58. byteAddr,
  59. byteLength,
  60. index,
  61. memType,
  62. memoryArea,
  63. sourceAddressText: formatAddress(byteAddr),
  64. typeName
  65. }
  66. }
  67. function parseCodeInfo(bytes) {
  68. const source = toBytes(bytes)
  69. if (source.length < FIXED_HEADER_BYTE_LENGTH) {
  70. throw new Error('info 信息块长度不足,无法解析 Modbus_Code_Info_t 固定头')
  71. }
  72. const byteLength = readUint16(source, 0)
  73. const structCount = readUint16(source, 40)
  74. const structEntryLength = readUint16(source, 42)
  75. if (structEntryLength < STRUCT_ENTRY_MIN_BYTE_LENGTH) {
  76. throw new Error('info 信息块 struct_entry_len 无效')
  77. }
  78. const availableTableBytes = Math.max(0, source.length - FIXED_HEADER_BYTE_LENGTH)
  79. const tableCapacity = Math.floor(availableTableBytes / structEntryLength)
  80. const safeStructCount = Math.min(structCount, tableCapacity)
  81. const structTable = []
  82. for (let index = 0; index < safeStructCount; index += 1) {
  83. structTable.push(parseStructEntry(
  84. source,
  85. FIXED_HEADER_BYTE_LENGTH + index * structEntryLength,
  86. structEntryLength,
  87. index
  88. ))
  89. }
  90. return {
  91. alongDiv: readFloat(source, 12),
  92. ampGain: readUint16(source, 4),
  93. busDiv: readFloat(source, 8),
  94. byteLength,
  95. caveFreq: source[2] & 0xFF,
  96. chipModel: readAscii(source, 16, 8),
  97. model: readUtf8OrAscii(source, 24, 16),
  98. rawHex: bytesToHex(source, ' '),
  99. refVolt: (source[3] & 0xFF) / 10,
  100. refVoltRaw: source[3] & 0xFF,
  101. rsShunt: readUint16(source, 6),
  102. structCount,
  103. structEntryLength,
  104. structTable,
  105. tableCapacity,
  106. truncated: safeStructCount < structCount
  107. }
  108. }
  109. function createRegistersForByteSpan(entry) {
  110. const count = Math.max(1, Number(entry.byteLength) || 1)
  111. const registers = []
  112. for (let offset = 0; offset < count; offset += 1) {
  113. registers.push({
  114. byteStart: offset,
  115. dataType: 'uint8_t',
  116. isPlaceholderByteField: true,
  117. name: offset.toString(16).toUpperCase().padStart(2, '0'),
  118. sourceAddress: (entry.byteAddr + offset) & 0xFFFF,
  119. sourceAddressText: formatAddress((entry.byteAddr + offset) & 0xFFFF),
  120. sourceByteLength: 1,
  121. sourceMemoryArea: entry.memoryArea,
  122. sourceMemoryClass: entry.memoryArea,
  123. sourceSymbolName: entry.typeName,
  124. sourceSymbolType: entry.typeName
  125. })
  126. }
  127. return registers
  128. }
  129. function createGroupsFromCodeInfo(codeInfo, options = {}) {
  130. const maxRegisters = Math.max(1, Number(options.maxRegistersPerGroup) || 256)
  131. const groups = []
  132. codeInfo.structTable.forEach((entry) => {
  133. if (!entry.byteLength || entry.memoryArea === 'UNKNOWN') return
  134. for (let offset = 0; offset < entry.byteLength; offset += maxRegisters) {
  135. const chunkLength = Math.min(maxRegisters, entry.byteLength - offset)
  136. const chunkEntry = {
  137. ...entry,
  138. byteAddr: (entry.byteAddr + offset) & 0xFFFF,
  139. byteLength: chunkLength
  140. }
  141. const suffix = entry.byteLength > maxRegisters ? ` #${Math.floor(offset / maxRegisters) + 1}` : ''
  142. groups.push({
  143. addressUnit: 'byte',
  144. layout: 'struct',
  145. name: `${entry.typeName}${suffix}`,
  146. quantity: chunkLength,
  147. registerType: entry.memoryArea === 'CODE' ? 'input' : 'holding',
  148. registers: createRegistersForByteSpan(chunkEntry),
  149. sourceAddress: chunkEntry.byteAddr,
  150. sourceAddressText: formatAddress(chunkEntry.byteAddr),
  151. sourceByteLength: chunkLength,
  152. sourceMemoryArea: entry.memoryArea,
  153. sourceMemoryClass: entry.memoryArea,
  154. sourceSegment: 'Modbus_Code_Info_t',
  155. sourceSegmentModule: '',
  156. sourceSymbolName: entry.typeName,
  157. sourceSymbolType: entry.typeName,
  158. startAddress: formatAddress(chunkEntry.byteAddr)
  159. })
  160. }
  161. })
  162. return groups
  163. }
  164. module.exports = {
  165. FIXED_HEADER_BYTE_LENGTH,
  166. createGroupsFromCodeInfo,
  167. parseCodeInfo
  168. }