1
0

view-model.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. const storageAccessProtocol = require('../../protocols/storage-access/index.js')
  2. const settingsService = require('../../store/settings-store.js')
  3. const {
  4. AREA
  5. } = storageAccessProtocol
  6. const {
  7. bytesToHex,
  8. bytesToUtf8Text,
  9. stringToUtf8Bytes
  10. } = require('../../utils/binary-utils.js')
  11. const LOG_MODE_OPTIONS = [
  12. { key: 'hex', label: 'HEX' },
  13. { key: 'text', label: '文本' }
  14. ]
  15. const STORAGE_ACCESS_COMMAND_OPTIONS = [
  16. { key: 'sync', label: '同步', description: '读取 info 并同步 code' },
  17. { key: 'read', label: '读取', description: '按字节读取内存' },
  18. { key: 'write', label: '写入', description: '按字节写入内存' }
  19. ]
  20. const STORAGE_ACCESS_AREA_OPTIONS = [
  21. { key: AREA.DATA, label: 'data' },
  22. { key: AREA.IDATA, label: 'idata' },
  23. { key: AREA.XDATA, label: 'xdata' },
  24. { key: AREA.CODE, label: 'code' }
  25. ]
  26. function normalizeHexText(value) {
  27. return String(value === undefined || value === null ? '' : value)
  28. .replace(/0x/gi, '')
  29. .replace(/[\s,;:_-]/g, ' ')
  30. .replace(/\s+/g, ' ')
  31. .trim()
  32. .toUpperCase()
  33. }
  34. function validateHexText(value) {
  35. const text = String(value === undefined || value === null ? '' : value).trim()
  36. const withoutPrefix = text.replace(/0x/gi, '')
  37. const compact = withoutPrefix.replace(/[\s,;:_-]/g, '')
  38. if (!compact) return '请输入十六进制数据'
  39. if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符'
  40. if (compact.length % 2 !== 0) return '十六进制长度必须为偶数'
  41. return ''
  42. }
  43. function parseHexBytes(value) {
  44. const compact = String(value === undefined || value === null ? '' : value)
  45. .trim()
  46. .replace(/0x/gi, '')
  47. .replace(/[\s,;:_-]/g, '')
  48. const bytes = []
  49. for (let index = 0; index < compact.length; index += 2) {
  50. bytes.push(parseInt(compact.slice(index, index + 2), 16) & 0xFF)
  51. }
  52. return bytes
  53. }
  54. function getSerialModeLabel(mode) {
  55. return mode === 'text' ? '文本' : 'HEX'
  56. }
  57. function getNextSerialMode(mode) {
  58. return mode === 'hex' ? 'text' : 'hex'
  59. }
  60. function getSerialModeToggleText(mode) {
  61. return getSerialModeLabel(getNextSerialMode(mode))
  62. }
  63. function getLogModeLabel(mode) {
  64. return mode === 'text' ? '文本' : 'HEX'
  65. }
  66. function getNextLogMode(mode) {
  67. return mode === 'hex' ? 'text' : 'hex'
  68. }
  69. function getLogModeToggleText(mode) {
  70. return getLogModeLabel(getNextLogMode(mode))
  71. }
  72. function normalizeSerialState(current = {}, changed = {}) {
  73. const next = {
  74. serialInputText: current.serialInputText || '',
  75. serialMode: current.serialMode || 'hex',
  76. ...changed
  77. }
  78. const mode = next.serialMode === 'text' ? 'text' : 'hex'
  79. const inputText = String(next.serialInputText || '')
  80. let errorText = ''
  81. let bytes = []
  82. let previewHex = ''
  83. if (inputText.trim()) {
  84. if (mode === 'hex') {
  85. errorText = validateHexText(inputText)
  86. if (!errorText) {
  87. bytes = parseHexBytes(inputText)
  88. previewHex = bytesToHex(bytes, ' ')
  89. }
  90. } else {
  91. bytes = stringToUtf8Bytes(inputText)
  92. previewHex = bytesToHex(bytes, ' ')
  93. }
  94. }
  95. return {
  96. serialErrorText: errorText,
  97. serialInputText: inputText,
  98. serialMode: mode,
  99. serialModeLabel: getSerialModeLabel(mode),
  100. serialModeToggleText: getSerialModeToggleText(mode),
  101. serialPreviewHex: previewHex,
  102. serialPreviewLengthText: bytes.length ? `${bytes.length} bytes` : '--'
  103. }
  104. }
  105. function formatAreaLabel(area) {
  106. const matched = STORAGE_ACCESS_AREA_OPTIONS.find((item) => item.key === area)
  107. return matched ? matched.label : 'data'
  108. }
  109. function buildStorageAccessPreview(commandMode, area, address, length, dataBytes) {
  110. try {
  111. if (commandMode === 'sync') {
  112. return bytesToHex(storageAccessProtocol.buildInfoFrame(), ' ')
  113. }
  114. if (commandMode === 'write') {
  115. return bytesToHex(storageAccessProtocol.buildWriteFrame(area, address, dataBytes), ' ')
  116. }
  117. return bytesToHex(storageAccessProtocol.buildReadFrame(area, address, length), ' ')
  118. } catch (error) {
  119. return ''
  120. }
  121. }
  122. function normalizeStorageAccessWordText(value, fallback) {
  123. const text = String(value === undefined || value === null ? '' : value).trim().replace(/^0x/i, '').toUpperCase()
  124. return text || fallback
  125. }
  126. function validateStorageAccessWordText(value, label) {
  127. const text = String(value || '').trim()
  128. if (!text) return `${label}请输入十六进制`
  129. if (!/^[0-9A-F]+$/i.test(text)) return `${label}只支持十六进制`
  130. if (text.length > 4) return `${label}最多 4 位十六进制`
  131. return ''
  132. }
  133. function formatStorageAccessWordText(value) {
  134. return Number(value || 0).toString(16).toUpperCase().padStart(4, '0')
  135. }
  136. function normalizeStorageAccessState(current = {}, changed = {}) {
  137. const next = {
  138. storageAccessAreaIndex: current.storageAccessAreaIndex || 0,
  139. storageAccessAddress: current.storageAccessAddress || '0000',
  140. storageAccessCommandIndex: current.storageAccessCommandIndex || 0,
  141. storageAccessDataText: current.storageAccessDataText || '',
  142. storageAccessLength: current.storageAccessLength || '0004',
  143. ...changed
  144. }
  145. const command = STORAGE_ACCESS_COMMAND_OPTIONS[Number(next.storageAccessCommandIndex) || 0] || STORAGE_ACCESS_COMMAND_OPTIONS[0]
  146. const area = STORAGE_ACCESS_AREA_OPTIONS[Number(next.storageAccessAreaIndex) || 0] || STORAGE_ACCESS_AREA_OPTIONS[0]
  147. const address = normalizeStorageAccessWordText(next.storageAccessAddress, '0000')
  148. const length = normalizeStorageAccessWordText(next.storageAccessLength, '0004')
  149. const dataText = normalizeHexText(next.storageAccessDataText)
  150. const dataBytes = dataText ? parseHexBytes(dataText) : []
  151. const previewArea = area.key
  152. const previewAddress = parseInt(address || '0', 16)
  153. const previewLength = parseInt(length || '0', 16)
  154. let errorText = ''
  155. if (command.key === 'read' || command.key === 'write') {
  156. const addressError = validateStorageAccessWordText(address, '地址')
  157. const lengthError = validateStorageAccessWordText(length, '长度')
  158. if (addressError) {
  159. errorText = addressError
  160. } else if (lengthError) {
  161. errorText = lengthError
  162. } else if (previewLength <= 0) {
  163. errorText = '长度必须大于 0'
  164. } else if (command.key === 'write') {
  165. const hexError = validateHexText(next.storageAccessDataText)
  166. if (hexError) {
  167. errorText = hexError
  168. } else if (dataBytes.length !== previewLength) {
  169. errorText = `写入长度为 ${previewLength} 字节,当前数据为 ${dataBytes.length} 字节`
  170. }
  171. }
  172. }
  173. if (command.key === 'sync') {
  174. const previewHex = buildStorageAccessPreview('sync', AREA.INFO, 0, 4, [])
  175. return {
  176. storageAccessAddress: '0000',
  177. storageAccessAreaIndex: 0,
  178. storageAccessAreaLabel: 'info',
  179. storageAccessCommandIndex: next.storageAccessCommandIndex,
  180. storageAccessCommandLabel: command.label,
  181. storageAccessDataText: '',
  182. storageAccessErrorText: '',
  183. storageAccessGeneratedHex: previewHex,
  184. storageAccessLength: '',
  185. storageAccessPreviewAreaText: 'info',
  186. storageAccessPreviewHexText: previewHex,
  187. storageAccessPreviewText: 'info 0x0000 / 4 bytes',
  188. storageAccessSendLabel: '同步',
  189. storageAccessSyncInfoText: '0x0F 读取 info[0:4]'
  190. }
  191. }
  192. const previewHex = command.key === 'write'
  193. ? buildStorageAccessPreview('write', previewArea, previewAddress, previewLength, dataBytes)
  194. : buildStorageAccessPreview('read', previewArea, previewAddress, previewLength, dataBytes)
  195. return {
  196. storageAccessAddress: address,
  197. storageAccessAreaIndex: next.storageAccessAreaIndex,
  198. storageAccessAreaLabel: area.label,
  199. storageAccessCommandIndex: next.storageAccessCommandIndex,
  200. storageAccessCommandLabel: command.label,
  201. storageAccessDataText: dataText,
  202. storageAccessErrorText: errorText,
  203. storageAccessGeneratedHex: errorText ? '' : previewHex,
  204. storageAccessLength: length,
  205. storageAccessPreviewAreaText: formatAreaLabel(previewArea),
  206. storageAccessPreviewHexText: errorText ? '' : previewHex,
  207. storageAccessPreviewText: errorText
  208. ? '--'
  209. : `${formatAreaLabel(previewArea)} 0x${formatStorageAccessWordText(previewAddress)} / ${previewLength} bytes`,
  210. storageAccessSendLabel: command.label,
  211. storageAccessSyncInfoText: ''
  212. }
  213. }
  214. function getLogPayloadText(log, mode) {
  215. if (!log) return '--'
  216. if (mode === 'text') {
  217. const payloadText = String(log.payloadText || '').trim()
  218. if (payloadText) return payloadText
  219. const bytes = Array.isArray(log.payloadBytes) ? log.payloadBytes : []
  220. return bytesToUtf8Text(bytes) || String(log.payload || '').trim() || '--'
  221. }
  222. if (typeof log.payload === 'string' && log.payload) {
  223. return log.payload
  224. }
  225. const bytes = Array.isArray(log.payloadBytes) ? log.payloadBytes : []
  226. return bytes.length ? bytesToHex(bytes, ' ') : '--'
  227. }
  228. function decorateLogs(logs, mode) {
  229. return (logs || []).map((item) => ({
  230. ...item,
  231. displayText: getLogPayloadText(item, mode)
  232. }))
  233. }
  234. function getProtocolModeState(settingsState = {}) {
  235. const isModbusProtocol = settingsService.isModbusProtocol(settingsState.protocolMode)
  236. const isStorageAccessProtocol = settingsService.isStorageAccessProtocol(settingsState.protocolMode)
  237. return {
  238. isModbusProtocol,
  239. isStorageAccessProtocol
  240. }
  241. }
  242. function getManualStatePayload(manualState = {}) {
  243. const commands = Array.isArray(manualState.protocolCommands) ? manualState.protocolCommands : []
  244. const command = commands[Number(manualState.commandIndex) || 0] || commands[0] || {}
  245. return {
  246. ...manualState,
  247. protocolCommandLabel: command.label || '指令',
  248. protocolResponseText: manualState.protocolResponseText || '',
  249. protocolSendLabel: '发送',
  250. showProtocolMultipleButton: command.inputMode === 'multiple',
  251. protocolTitleText: '标准 Modbus'
  252. }
  253. }
  254. module.exports = {
  255. LOG_MODE_OPTIONS,
  256. STORAGE_ACCESS_AREA_OPTIONS,
  257. STORAGE_ACCESS_COMMAND_OPTIONS,
  258. decorateLogs,
  259. getLogModeToggleText,
  260. getManualStatePayload,
  261. getNextLogMode,
  262. getNextSerialMode,
  263. getProtocolModeState,
  264. normalizeSerialState,
  265. normalizeHexText,
  266. normalizeStorageAccessState,
  267. parseHexBytes,
  268. validateHexText
  269. }