diff --git a/Release/Models/libXNATA04DataProcessor.so.1.0.0.0 b/Release/Models/libXNATA04DataProcessor.so.1.0.0.0 index b867af2..d7dbcf7 100644 Binary files a/Release/Models/libXNATA04DataProcessor.so.1.0.0.0 and b/Release/Models/libXNATA04DataProcessor.so.1.0.0.0 differ diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index be98c45..07e7c1a 100644 Binary files a/Release/database/XNSim.db and b/Release/database/XNSim.db differ diff --git a/XNSimHtml/components/network-monitor.js b/XNSimHtml/components/network-monitor.js index 7f33b15..2dbbab2 100644 --- a/XNSimHtml/components/network-monitor.js +++ b/XNSimHtml/components/network-monitor.js @@ -13,6 +13,7 @@ class NetworkMonitor extends HTMLElement { data: [], timer: null, statusMsg: '', + selectedData: null, }; } @@ -37,7 +38,25 @@ class NetworkMonitor extends HTMLElement { border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); padding: 24px 20px 20px 20px; - max-width: 600px; + width: 100%; + min-width: 400px; + max-width: 100%; + height: 100%; + overflow-y: auto; + box-sizing: border-box; + } + .container { + display: flex; + height: 100%; + gap: 20px; + } + .left-panel { + flex: 1; + min-width: 0; + } + .right-panel { + flex: 1; + min-width: 0; } .form-row { display: flex; @@ -118,13 +137,16 @@ class NetworkMonitor extends HTMLElement { color: #e53935; } .data-list { - max-height: 340px; + height: calc(100% - 143px); + min-height: 200px; overflow-y: auto; background: #fff; border-radius: 10px; border: 1px solid #e0e3e7; padding: 10px 0 10px 0; box-shadow: 0 1px 6px rgba(0,0,0,0.04); + width: 100%; + box-sizing: border-box; } .data-item { background: #f8fafc; @@ -135,36 +157,146 @@ class NetworkMonitor extends HTMLElement { border-left: 4px solid #409eff; font-size: 14px; transition: box-shadow 0.2s; + cursor: pointer; } .data-item:hover { box-shadow: 0 2px 8px rgba(64,158,255,0.13); } + .data-item.selected { + background: #e3f2fd; + border-left-color: #1976d2; + box-shadow: 0 2px 8px rgba(25,118,210,0.2); + } .data-item b { color: #1976d2; } - .data-item pre { - background: #f3f7fa; - border-radius: 4px; - padding: 4px 6px; - margin: 2px 0 0 0; + .detail-panel { + background: #fff; + border-radius: 10px; + border: 1px solid #e0e3e7; + padding: 20px; + box-shadow: 0 1px 6px rgba(0,0,0,0.04); + height: calc(100% - 40px); + overflow-y: auto; + } + .detail-header { + margin-bottom: 6px; + padding-bottom: 4px; + border-bottom: 2px solid #e3f2fd; + } + .detail-header h2 { + margin: 0 0 10px 0; + color: #1976d2; + font-size: 20px; + font-weight: 600; + } + .hex-content { + background: #f8fafc; + border-radius: 8px; + padding: 16px; + font-family: 'Courier New', monospace; font-size: 13px; - color: #333; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-all; + border: 1px solid #e0e3e7; + height: 250px; + max-height: 250px; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; + } + .parse-content { + background: #f8fafc; + border-radius: 8px; + padding: 16px; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + line-height: 1.6; + border: 1px solid #e0e3e7; + margin-top: 16px; + max-height: 400px; + overflow-y: auto; + } + .parse-item { + margin-bottom: 12px; + padding: 8px 12px; + background: #fff; + border-radius: 6px; + border-left: 4px solid #409eff; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + } + .parse-item b { + color: #1976d2; + margin-right: 8px; + font-size: 16px; + } + .parse-item .hex-value { + background: #e3f2fd; + padding: 2px 6px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 12px; + color: #1976d2; + margin: 0 4px; + } + .hex-content .offset { + color: #666; + font-weight: bold; + } + .hex-content .hex { + color: #1976d2; + } + .no-selection { + text-align: center; + padding: 60px 20px; + color: #999; + font-style: italic; + font-size: 16px; + } + .no-selection::before { + content: '📋'; + display: block; + font-size: 48px; + margin-bottom: 16px; } .nodata { color: #bbb; text-align: center; padding: 30px 0; } + .detail-title { + font-size: 16px; + font-weight: 600; + color: #2c3e50; + margin-bottom: 4px; + padding: 8px 0; + border-bottom: 2px solid #409eff; + } -
${this.state.statusMsg}
-
- - - - +
+
+
${this.state.statusMsg}
+
+ + + + +
+
抓包状态:${this.state.isMonitoring ? '运行中' : '已停止'}
+
+
+
+
+
+

UDP数据包详情

+
+
+
请从左侧选择一个数据包查看详情
+
+
+
-
抓包状态:${this.state.isMonitoring ? '运行中' : '已停止'}
-
`; this.shadowRoot.getElementById('startBtn').onclick = () => this.startMonitor(); this.shadowRoot.getElementById('stopBtn').onclick = () => this.stopMonitor(); @@ -184,13 +316,67 @@ class NetworkMonitor extends HTMLElement { dataList.innerHTML = '
暂无数据
'; return; } - dataList.innerHTML = this.state.data.map(item => ` -
-
时间: ${new Date(item.timestamp).toLocaleString()}
-
来源: ${item.source}
-
内容:
${typeof item.data === 'object' ? JSON.stringify(item.data, null, 2) : item.data}
+ dataList.innerHTML = this.state.data.map((item, index) => ` +
+ + + + + + +
时间: ${new Date(item.timestamp).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3 + })}来源: ${item.source}大小: ${item.length || this.getDataSize(item.data)} 字节
`).join(''); + + // 添加点击事件 + dataList.querySelectorAll('.data-item').forEach(item => { + item.addEventListener('click', (e) => { + const index = parseInt(e.currentTarget.dataset.index); + this.selectDataItem(index); + }); + }); + } + + getDataSize(data) { + // 如果数据是十六进制字符串,每两个字符代表一个字节 + if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) { + return Math.ceil(data.length / 2); + } + return 0; + } + + formatDataAsHex(data) { + // 如果数据是十六进制字符串,直接处理 + if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) { + const bytes = []; + for (let i = 0; i < data.length; i += 2) { + const byte = parseInt(data.substr(i, 2), 16); + bytes.push(byte); + } + + // 分行显示,每行16个字节 + const bytesPerLine = 16; + const lines = []; + + for (let i = 0; i < bytes.length; i += bytesPerLine) { + const lineBytes = bytes.slice(i, i + bytesPerLine); + const lineHex = lineBytes.map(byte => byte.toString(16).padStart(2, '0')).join(' '); + const offset = i.toString(16).padStart(4, '0').toUpperCase(); + lines.push(`${offset}: ${lineHex}`); + } + + return lines.join('\n'); + } + + return '无效数据格式'; } setStatus(msg) { @@ -257,6 +443,7 @@ class NetworkMonitor extends HTMLElement { this.state.data = this.state.data.slice(-1000); } this.renderDataList(); + this.updateDetailContent(); } this.state.isMonitoring = data.isMonitoring; this.shadowRoot.getElementById('monitorStatus').textContent = data.isMonitoring ? '运行中' : '已停止'; @@ -284,6 +471,141 @@ class NetworkMonitor extends HTMLElement { reactivate() { this.initialize(); } + + selectDataItem(index) { + this.state.selectedData = index; + this.renderDataList(); + this.updateDetailContent(); + } + + updateDetailContent() { + const detailContent = this.shadowRoot.getElementById('detailContent'); + if (!detailContent) return; + + if (this.state.selectedData === null || !this.state.data[this.state.selectedData]) { + detailContent.innerHTML = '
请从左侧选择一个数据包查看详情
'; + return; + } + + const item = this.state.data[this.state.selectedData]; + detailContent.innerHTML = ` +
${this.formatHexContent(item.data)}
+
+ ${this.formatParseContent(item.data)} +
+ `; + } + + getPacketIndex(packet) { + // 这里可以根据需要实现数据包序号逻辑 + return Math.floor(Math.random() * 1000) + 1; + } + + formatHexContent(data) { + // 如果数据是十六进制字符串,直接处理 + if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) { + const bytes = []; + for (let i = 0; i < data.length; i += 2) { + const byte = parseInt(data.substr(i, 2), 16); + bytes.push(byte); + } + + // 分行显示,每行16个字节 + const bytesPerLine = 16; + const lines = []; + + for (let i = 0; i < bytes.length; i += bytesPerLine) { + const lineBytes = bytes.slice(i, i + bytesPerLine); + const lineHex = lineBytes.map(byte => byte.toString(16).padStart(2, '0')).join(' '); + const offset = i.toString(16).padStart(4, '0').toUpperCase(); + lines.push(`${offset}: ${lineHex}`); + } + + return lines.join('\n'); + } + + return '无效数据格式'; + } + + formatParseContent(data) { + if (typeof data !== 'string' || !/^[0-9a-fA-F]+$/.test(data) || data.length < 14) { + return '
数据包长度不足,无法解析
'; + } + + const bytes = []; + for (let i = 0; i < data.length; i += 2) { + const byte = parseInt(data.substr(i, 2), 16); + bytes.push(byte); + } + + // 解析数据包头 + const header = bytes.slice(0, 6); // 前6个字节 + const sizeBytes = bytes.slice(6, 8); // 第7-8个字节表示数据包大小 + + let parseResult = '
数据包头解析
'; + + // 1. a6-XNSim数据 + parseResult += `
1. ${header[0].toString(16).padStart(2, '0')} - XNSim数据
`; + + // 2. c0-C909数据 c1-C919数据 + const dataType = header[1]; + let dataTypeDesc = ''; + if (dataType === 0xc0) { + dataTypeDesc = 'C909数据'; + } else if (dataType === 0xc1) { + dataTypeDesc = 'C919数据'; + } else { + dataTypeDesc = `未知数据类型(${dataType.toString(16).padStart(2, '0')})`; + } + parseResult += `
2. ${dataType.toString(16).padStart(2, '0')} - ${dataTypeDesc}
`; + + // 3. ATA章节号 + const ataChapter = header[2]; + parseResult += `
3. ${ataChapter.toString(16).padStart(2, '0')} - ATA章节号 (ATA${ataChapter.toString().padStart(2, '0')})
`; + + // 4. 模型编号 + const modelNumber = header[3]; + parseResult += `
4. ${modelNumber.toString(16).padStart(2, '0')} - 模型编号 (${modelNumber})
`; + + // 5. 结构体类别 + const structType = header[4]; + let structTypeDesc = ''; + switch (structType) { + case 0x00: + structTypeDesc = '输入结构体'; + break; + case 0x01: + structTypeDesc = '输出结构体'; + break; + case 0x02: + structTypeDesc = '心跳结构体'; + break; + default: + structTypeDesc = `未知结构体类型(${structType.toString(16).padStart(2, '0')})`; + } + parseResult += `
5. ${structType.toString(16).padStart(2, '0')} - 结构体类别 (${structTypeDesc})
`; + + // 6. 数据传输方向 + const direction = header[5]; + let directionDesc = ''; + switch (direction) { + case 0x00: + directionDesc = '输入'; + break; + case 0x01: + directionDesc = '输出'; + break; + default: + directionDesc = `未知方向(${direction.toString(16).padStart(2, '0')})`; + } + parseResult += `
6. ${direction.toString(16).padStart(2, '0')} - 数据传输方向 (${directionDesc})
`; + + // 7. 数据包大小 + const packetSize = (sizeBytes[0] << 8) | sizeBytes[1]; + parseResult += `
7. ${sizeBytes[0].toString(16).padStart(2, '0')} ${sizeBytes[1].toString(16).padStart(2, '0')} - 数据包大小 (${packetSize} 字节)
`; + + return parseResult; + } } -customElements.define('network-monitor', NetworkMonitor); \ No newline at end of file +customElements.define('network-monitor', NetworkMonitor); \ No newline at end of file diff --git a/XNSimHtml/routes/udp-monitor.js b/XNSimHtml/routes/udp-monitor.js index dc749e9..49ac97b 100644 --- a/XNSimHtml/routes/udp-monitor.js +++ b/XNSimHtml/routes/udp-monitor.js @@ -55,21 +55,17 @@ router.post('/start', (req, res) => { udpServer.on('message', (msg, rinfo) => { // 将收到的数据添加到数据队列 try { - let processedData; - - // 尝试将数据解析为JSON - try { - processedData = JSON.parse(msg.toString()); - } catch (e) { - // 如果不是JSON,则保存为字符串 - processedData = msg.toString(); - } + // 保存原始字节数据,转换为十六进制字符串 + const hexData = msg.toString('hex'); + const rawBytes = Array.from(msg); // 将数据添加到队列 udpData.push({ timestamp: Date.now(), source: `${rinfo.address}:${rinfo.port}`, - data: processedData + data: hexData, + rawBytes: rawBytes, + length: msg.length }); // 限制数据队列大小,最多保存1000条记录 diff --git a/XNSimHtml/utils/model-utils.js b/XNSimHtml/utils/model-utils.js index b10d11c..be86f80 100644 --- a/XNSimHtml/utils/model-utils.js +++ b/XNSimHtml/utils/model-utils.js @@ -233,10 +233,7 @@ function saveNewVersion(db, versionData) { versionData.DataPackageEntryPoint || '', versionData.DataPackageInterfaceName || '', versionData.ConfID, - versionData.CmdList || '[]', - versionData.ClassName, - versionData.originalVersion || versionData.Version, - versionData.PlaneName + versionData.CmdList || '[]' ); return {