<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Serial NMEA 数据监控台</title> <style> body { font-family: Arial, sans-serif; padding: 20px; max-width: 900px; margin: auto; background-color: #f8f9fa; } h2 { color: #333; } /* 控制区域样式 */ .controls { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; flex-wrap: wrap; } button { padding: 8px 15px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; transition: 0.2s; } button:hover { opacity: 0.9; } button:disabled { background-color: #ccc; cursor: not-allowed; } button.disconnect { background-color: #dc3545; } button.secondary { background-color: #6c757d; } /* 控制台显示区样式 */ textarea { width: 100%; height: 250px; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-family: 'Courier New', monospace; resize: vertical; box-sizing: border-box; background-color: #fff; } input[type="text"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; flex-grow: 1; min-width: 200px; } /* 底部 NMEA 数据面板样式 */ .nMEA-panel { margin-top: 30px; padding: 20px; background-color: #fff; border: 2px solid #007bff; border-radius: 8px; } .nMEA-panel h3 { margin-top: 0; color: #007bff; border-bottom: 1px solid #eee; padding-bottom: 10px; } /* 底部 NMEA 数据面板样式 */ .data-grid { display: grid; grid-template-columns: 1fr; /* 核心修改:强制为单列布局 */ gap: 15px; margin-top: 15px; } .data-item { padding: 10px; background-color: #f1f3f5; border-radius: 4px; word-break: break-all; /* 允许长字符串自动换行 */ white-space: normal; /* 取消单行限制 */ } .data-item strong { color: #495057; display: block; margin-bottom: 4px; font-size: 12px; text-transform: uppercase; } .data-item span { font-family: 'Courier New', monospace; color: #333; font-size: 14px; transition: color 0.3s; } @media (max-width: 600px) { .data-grid { grid-template-columns: 1fr; } } </style> </head> <body> <h2>🛰️ Web Serial NMEA 数据监控台</h2> <!-- 1. 串口控制区域 --> <div class="controls"> <button id="connectBtn">🔌 连接串口</button> <button id="disconnectBtn" class="disconnect" disabled>断开连接</button> </div> <!-- 2. 数据接收显示区 --> <textarea id="output" readonly placeholder="等待串口数据..."></textarea> <!-- 3. 数据发送区域 --> <div class="controls" style="margin-top: 15px;"> <input type="text" id="inputData" placeholder="输入要发送的指令..."> <button id="sendBtn" disabled>发送</button> <button id="clearBtn" class="secondary">清空控制台</button> </div> <!-- 4. 底部 NMEA 数据实时显示面板 --> <div class="nMEA-panel"> <h3>📡 实时协议数据解析</h3> <div class="data-grid"> <div class="data-item"> <strong>RMC (推荐最小定位信息)</strong> <span id="RMC">等待接收...</span> </div> <div class="data-item"> <strong>DHV (自定义航向/速度)</strong> <span id="DHV">等待接收...</span> </div> <div class="data-item"> <strong>GGA (GPS 定位数据)</strong> <span id="GGA">等待接收...</span> </div> <div class="data-item"> <strong>GSA (卫星状态及精度)</strong> <span id="GSA">等待接收...</span> </div> <div class="data-item"> <strong>ARS (自动相关监视)</strong> <span id="ARS">等待接收...</span> </div> <div class="data-item"> <strong>HTX304P</strong> <span id="HTX304P">等待接收...</span> </div> <div class="data-item"> <strong>AUT</strong> <span id="AUT">等待接收...</span> </div> </div> </div> <script> // ================= 核心变量与 DOM 元素 ================= let port = null; let reader = null; let isConnected = false; const connectBtn = document.getElementById('connectBtn'); const disconnectBtn = document.getElementById('disconnectBtn'); const sendBtn = document.getElementById('sendBtn'); const clearBtn = document.getElementById('clearBtn'); const output = document.getElementById('output'); const inputData = document.getElementById('inputData'); // ================= 1. 协议配置表 (核心解析逻辑) ================= const PROTOCOL_MAP = [ { prefix: '$BDAUT', elementId: 'AUT' }, { prefix: '$GNAUT', elementId: 'AUT' }, { prefix: 'HTX304P', elementId: 'HTX304P' }, { prefix: '$BDRMC', elementId: 'RMC' }, { prefix: '$GNRMC', elementId: 'RMC' }, { prefix: '$BDDHV', elementId: 'DHV' }, { prefix: '$GNDHV', elementId: 'DHV' }, { prefix: '$BDGGA', elementId: 'GGA' }, { prefix: '$GNGGA', elementId: 'GGA' }, { prefix: '$BDGSA', elementId: 'GSA' }, { prefix: '$GNGSA', elementId: 'GSA' }, { prefix: '$BDARS', elementId: 'ARS' }, { prefix: '$GNARS', elementId: 'ARS' } ]; // 解析单行数据的函数 function parseLine(line) { const trimmedLine = line.trim(); if (trimmedLine === '') return; appendOutput(`📥 [解析] ${trimmedLine}\n`); // 遍历配置表,匹配前缀并更新对应的 DOM 元素 for (const protocol of PROTOCOL_MAP) { if (trimmedLine.startsWith(protocol.prefix)) { const element = document.getElementById(protocol.elementId); if (element) { element.textContent = trimmedLine; element.style.color = '#007bff'; // 视觉高亮反馈 } break; // 匹配成功即跳出,提升性能 } } } // ================= 2. 串口连接与断开 ================= connectBtn.addEventListener('click', async () => { try { port = await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); // 请根据实际设备修改波特率 isConnected = true; updateUI(true); appendOutput('✅ 串口连接成功!\n'); readLoop(); } catch (e) { appendOutput('❌ 连接失败: ' + e.message + '\n'); } }); disconnectBtn.addEventListener('click', async () => { try { if (reader) { await reader.cancel(); reader.releaseLock(); reader = null; } if (port) { await port.close(); port = null; } isConnected = false; updateUI(false); appendOutput('🔌 串口已断开。\n'); } catch (e) { appendOutput('❌ 断开失败: ' + e.message + '\n'); } }); // ================= 3. 数据发送 ================= sendBtn.addEventListener('click', async () => { if (!port || !isConnected) return; const text = inputData.value; if (!text) return; try { const writer = port.writable.getWriter(); const encoder = new TextEncoder(); await writer.write(encoder.encode(text + '\n')); writer.releaseLock(); appendOutput(`📤 发送: ${text}\n`); inputData.value = ''; } catch (e) { appendOutput('❌ 发送失败: ' + e.message + '\n'); } }); clearBtn.addEventListener('click', () => { output.value = ''; }); // ================= 4. 持续读取与按行解析循环 ================= async function readLoop() { const decoder = new TextDecoder(); let buffer = ''; // 维护数据缓冲区 while (port && port.readable && isConnected) { try { reader = port.readable.getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; if (value) { buffer += decoder.decode(value, { stream: true }); let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.substring(0, newlineIndex); parseLine(line); // 触发解析 buffer = buffer.substring(newlineIndex + 1); } } } } catch (e) { appendOutput('⚠️ 读取中断: ' + e.message + '\n'); } finally { if (reader) { reader.releaseLock(); reader = null; } } } } // ================= 辅助方法 ================= function updateUI(connected) { connectBtn.disabled = connected; disconnectBtn.disabled = !connected; sendBtn.disabled = !connected; } function appendOutput(text) { output.value += text; output.scrollTop = output.scrollHeight; } </script> </body> </html>
本文作者:Kellermen
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!