const storageAccessProtocol = require('../../protocols/storage-access/index.js') const settingsService = require('../../store/settings-store.js') const { AREA } = storageAccessProtocol const { bytesToHex, bytesToUtf8Text, stringToUtf8Bytes } = require('../../utils/binary-utils.js') const LOG_MODE_OPTIONS = [ { key: 'hex', label: 'HEX' }, { key: 'text', label: '文本' } ] const STORAGE_ACCESS_COMMAND_OPTIONS = [ { key: 'sync', label: '同步', description: '读取 info 并同步 code' }, { key: 'read', label: '读取', description: '按字节读取内存' }, { key: 'write', label: '写入', description: '按字节写入内存' } ] const STORAGE_ACCESS_AREA_OPTIONS = [ { key: AREA.DATA, label: 'data' }, { key: AREA.IDATA, label: 'idata' }, { key: AREA.XDATA, label: 'xdata' }, { key: AREA.CODE, label: 'code' } ] function normalizeHexText(value) { return String(value === undefined || value === null ? '' : value) .replace(/0x/gi, '') .replace(/[\s,;:_-]/g, ' ') .replace(/\s+/g, ' ') .trim() .toUpperCase() } function validateHexText(value) { const text = String(value === undefined || value === null ? '' : value).trim() const withoutPrefix = text.replace(/0x/gi, '') const compact = withoutPrefix.replace(/[\s,;:_-]/g, '') if (!compact) return '请输入十六进制数据' if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符' if (compact.length % 2 !== 0) return '十六进制长度必须为偶数' return '' } function parseHexBytes(value) { const compact = String(value === undefined || value === null ? '' : value) .trim() .replace(/0x/gi, '') .replace(/[\s,;:_-]/g, '') const bytes = [] for (let index = 0; index < compact.length; index += 2) { bytes.push(parseInt(compact.slice(index, index + 2), 16) & 0xFF) } return bytes } function getSerialModeLabel(mode) { return mode === 'text' ? '文本' : 'HEX' } function getNextSerialMode(mode) { return mode === 'hex' ? 'text' : 'hex' } function getSerialModeToggleText(mode) { return getSerialModeLabel(getNextSerialMode(mode)) } function getLogModeLabel(mode) { return mode === 'text' ? '文本' : 'HEX' } function getNextLogMode(mode) { return mode === 'hex' ? 'text' : 'hex' } function getLogModeToggleText(mode) { return getLogModeLabel(getNextLogMode(mode)) } function normalizeSerialState(current = {}, changed = {}) { const next = { serialInputText: current.serialInputText || '', serialMode: current.serialMode || 'hex', ...changed } const mode = next.serialMode === 'text' ? 'text' : 'hex' const inputText = String(next.serialInputText || '') let errorText = '' let bytes = [] let previewHex = '' if (inputText.trim()) { if (mode === 'hex') { errorText = validateHexText(inputText) if (!errorText) { bytes = parseHexBytes(inputText) previewHex = bytesToHex(bytes, ' ') } } else { bytes = stringToUtf8Bytes(inputText) previewHex = bytesToHex(bytes, ' ') } } return { serialErrorText: errorText, serialInputText: inputText, serialMode: mode, serialModeLabel: getSerialModeLabel(mode), serialModeToggleText: getSerialModeToggleText(mode), serialPreviewHex: previewHex, serialPreviewLengthText: bytes.length ? `${bytes.length} bytes` : '--' } } function formatAreaLabel(area) { const matched = STORAGE_ACCESS_AREA_OPTIONS.find((item) => item.key === area) return matched ? matched.label : 'data' } function buildStorageAccessPreview(commandMode, area, address, length, dataBytes) { try { if (commandMode === 'sync') { return bytesToHex(storageAccessProtocol.buildInfoFrame(), ' ') } if (commandMode === 'write') { return bytesToHex(storageAccessProtocol.buildWriteFrame(area, address, dataBytes), ' ') } return bytesToHex(storageAccessProtocol.buildReadFrame(area, address, length), ' ') } catch (error) { return '' } } function normalizeStorageAccessWordText(value, fallback) { const text = String(value === undefined || value === null ? '' : value).trim().replace(/^0x/i, '').toUpperCase() return text || fallback } function validateStorageAccessWordText(value, label) { const text = String(value || '').trim() if (!text) return `${label}请输入十六进制` if (!/^[0-9A-F]+$/i.test(text)) return `${label}只支持十六进制` if (text.length > 4) return `${label}最多 4 位十六进制` return '' } function formatStorageAccessWordText(value) { return Number(value || 0).toString(16).toUpperCase().padStart(4, '0') } function normalizeStorageAccessState(current = {}, changed = {}) { const next = { storageAccessAreaIndex: current.storageAccessAreaIndex || 0, storageAccessAddress: current.storageAccessAddress || '0000', storageAccessCommandIndex: current.storageAccessCommandIndex || 0, storageAccessDataText: current.storageAccessDataText || '', storageAccessLength: current.storageAccessLength || '0004', ...changed } const command = STORAGE_ACCESS_COMMAND_OPTIONS[Number(next.storageAccessCommandIndex) || 0] || STORAGE_ACCESS_COMMAND_OPTIONS[0] const area = STORAGE_ACCESS_AREA_OPTIONS[Number(next.storageAccessAreaIndex) || 0] || STORAGE_ACCESS_AREA_OPTIONS[0] const address = normalizeStorageAccessWordText(next.storageAccessAddress, '0000') const length = normalizeStorageAccessWordText(next.storageAccessLength, '0004') const dataText = normalizeHexText(next.storageAccessDataText) const dataBytes = dataText ? parseHexBytes(dataText) : [] const previewArea = area.key const previewAddress = parseInt(address || '0', 16) const previewLength = parseInt(length || '0', 16) let errorText = '' if (command.key === 'read' || command.key === 'write') { const addressError = validateStorageAccessWordText(address, '地址') const lengthError = validateStorageAccessWordText(length, '长度') if (addressError) { errorText = addressError } else if (lengthError) { errorText = lengthError } else if (previewLength <= 0) { errorText = '长度必须大于 0' } else if (command.key === 'write') { const hexError = validateHexText(next.storageAccessDataText) if (hexError) { errorText = hexError } else if (dataBytes.length !== previewLength) { errorText = `写入长度为 ${previewLength} 字节,当前数据为 ${dataBytes.length} 字节` } } } if (command.key === 'sync') { const previewHex = buildStorageAccessPreview('sync', AREA.INFO, 0, 4, []) return { storageAccessAddress: '0000', storageAccessAreaIndex: 0, storageAccessAreaLabel: 'info', storageAccessCommandIndex: next.storageAccessCommandIndex, storageAccessCommandLabel: command.label, storageAccessDataText: '', storageAccessErrorText: '', storageAccessGeneratedHex: previewHex, storageAccessLength: '', storageAccessPreviewAreaText: 'info', storageAccessPreviewHexText: previewHex, storageAccessPreviewText: 'info 0x0000 / 4 bytes', storageAccessSendLabel: '同步', storageAccessSyncInfoText: '0x0F 读取 info[0:4]' } } const previewHex = command.key === 'write' ? buildStorageAccessPreview('write', previewArea, previewAddress, previewLength, dataBytes) : buildStorageAccessPreview('read', previewArea, previewAddress, previewLength, dataBytes) return { storageAccessAddress: address, storageAccessAreaIndex: next.storageAccessAreaIndex, storageAccessAreaLabel: area.label, storageAccessCommandIndex: next.storageAccessCommandIndex, storageAccessCommandLabel: command.label, storageAccessDataText: dataText, storageAccessErrorText: errorText, storageAccessGeneratedHex: errorText ? '' : previewHex, storageAccessLength: length, storageAccessPreviewAreaText: formatAreaLabel(previewArea), storageAccessPreviewHexText: errorText ? '' : previewHex, storageAccessPreviewText: errorText ? '--' : `${formatAreaLabel(previewArea)} 0x${formatStorageAccessWordText(previewAddress)} / ${previewLength} bytes`, storageAccessSendLabel: command.label, storageAccessSyncInfoText: '' } } function getLogPayloadText(log, mode) { if (!log) return '--' if (mode === 'text') { const payloadText = String(log.payloadText || '').trim() if (payloadText) return payloadText const bytes = Array.isArray(log.payloadBytes) ? log.payloadBytes : [] return bytesToUtf8Text(bytes) || String(log.payload || '').trim() || '--' } if (typeof log.payload === 'string' && log.payload) { return log.payload } const bytes = Array.isArray(log.payloadBytes) ? log.payloadBytes : [] return bytes.length ? bytesToHex(bytes, ' ') : '--' } function decorateLogs(logs, mode) { return (logs || []).map((item) => ({ ...item, displayText: getLogPayloadText(item, mode) })) } function getProtocolModeState(settingsState = {}) { const isModbusProtocol = settingsService.isModbusProtocol(settingsState.protocolMode) const isStorageAccessProtocol = settingsService.isStorageAccessProtocol(settingsState.protocolMode) return { isModbusProtocol, isStorageAccessProtocol } } function getManualStatePayload(manualState = {}) { const commands = Array.isArray(manualState.protocolCommands) ? manualState.protocolCommands : [] const command = commands[Number(manualState.commandIndex) || 0] || commands[0] || {} return { ...manualState, protocolCommandLabel: command.label || '指令', protocolResponseText: manualState.protocolResponseText || '', protocolSendLabel: '发送', showProtocolMultipleButton: command.inputMode === 'multiple', protocolTitleText: '标准 Modbus' } } module.exports = { LOG_MODE_OPTIONS, STORAGE_ACCESS_AREA_OPTIONS, STORAGE_ACCESS_COMMAND_OPTIONS, decorateLogs, getLogModeToggleText, getManualStatePayload, getNextLogMode, getNextSerialMode, getProtocolModeState, normalizeSerialState, normalizeHexText, normalizeStorageAccessState, parseHexBytes, validateHexText }