使用JavaScript处理串口数据(Modbus协议)
Modbus RTU 主站 - 稳定轮询(后台读泵 + 帧队列)

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <title>Modbus RTU 主站 - 稳定轮询(后台读泵 + 帧队列)</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> <style> body { font-family: Arial, sans-serif; padding: 20px; } .config-group { margin-bottom: 12px; } .config-group label { display:inline-block; width:140px; } .config-group input { width:160px; } #connectButton { padding:8px 16px; background:red; color:white; border:none; cursor:pointer; } #connectButton.connected { background:green; } #connectionInfo { font-size: small; color: gray; } #dataDisplay { margin-top:16px; padding:12px; border:1px solid #ccc; border-radius:6px; font-family:monospace; white-space:pre-wrap; display: flex; justify-content: space-between; } #dataDisplay > div { flex: 1; padding: 0 10px; } #dataDisplay > div:first-child { border-right: 1px solid #ccc; } #counters { margin-left: 12px; font-size: 13px; color: #333; } #counters span { font-weight: bold; margin-right: 8px; } #toolbar { margin-top: 8px; } #resetCountersBtn { margin-left: 8px; } </style> </head> <body> <h3>Modbus RTU 主站 — 稳定轮询(后台读泵 + 帧队列)</h3> <div class="config-group"> <label>波特率:</label><input id="baudRateInput" type="number" value="115200"> </div> <div class="config-group"> <label>从机地址:</label><input id="slaveAddressInput" type="number" value="1"> </div> <div class="config-group"> <label>间隔时间 (ms):</label><input id="intervalInput" type="number" value="1"> </div> <div class="config-group"> <label>数据长度 (寄存器数):</label><input id="numRegsInput" type="number" value="10"> </div> <div class="config-group"> <label>超时时间 (ms):</label><input id="timeoutInput" type="number" value="2000"> </div> <div class="config-group"> <label>写保持寄存器地址:</label><input id="writeAddressInput" type="number" value="0"> </div> <div class="config-group"> <label>写保持寄存器值:</label><input id="writeValueInput" type="number" value="0"> </div> <div id="toolbar"> <button id="connectButton" onclick="toggleConnection()">连接设备</button> <span id="connectionInfo"></span> <span id="counters">发送: <span id="sendCount">0</span> 失败: <span id="failCount">0</span></span> <button id="resetCountersBtn" onclick="resetCounters()">重置计数</button> </div> <div style="margin-top:8px;"> <button id="writeButton" onclick="setPendingWrite()" disabled>写保持寄存器 (0x06)</button> </div> <div id="dataDisplay">无数据</div> <script> /* 后台读泵 + 帧队列 说明(要点) - readPump(): 持续读取串口字节,追加到 bufferBytes - parseBufferToFrames(): 尝试从 bufferBytes 中提取完整 Modbus 帧(优先按 func 判断长度;idle fallback) - framesQueue: 已解析但未消费的完整帧,sendAndWait() 从中匹配期望的响应 - sendAndWait(): 发送数据,等待 framesQueue 中匹配的响应(按 slave & func) - CRC 校验在解析阶段进行,避免噪声或半帧误判 */ let port = null; let isConnected = false; let baudRate = 115200; let slaveAddress = 1; let intervalMs = 500; let numRegs = 10; let timeoutMs = 500; // 串口读泵/解析状态 let activeReader = null; let readPumpTask = null; let bufferBytes = new Uint8Array(0); // 未解析的字节缓存 let framesQueue = []; // { frame: Uint8Array, ts: number } // 写控制 let activeWriter = null; let pendingWrite = false; let pendingWriteAddress = 0; let pendingWriteValue = 0; // 计数器 let sendCount = 0; let failCount = 0; // DOM const connectBtn = document.getElementById('connectButton'); const connInfo = document.getElementById('connectionInfo'); const dataDisplay = document.getElementById('dataDisplay'); const writeBtn = document.getElementById('writeButton'); const sendCountEl = document.getElementById('sendCount'); const failCountEl = document.getElementById('failCount'); let lastInputData = '无数据'; let lastHoldingData = '无数据'; // CRC16 (Modbus) function calculateCRC(bytes) { let crc = 0xFFFF; for (let i = 0; i < bytes.length; i++) { crc ^= bytes[i]; for (let j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; } function checkCRC(frame) { if (!frame || frame.length < 3) return false; const len = frame.length; const crcIn = (frame[len-1] << 8) | frame[len-2]; const crcCalc = calculateCRC(frame.slice(0, len-2)); return crcIn === crcCalc; } function toHex(arr) { return Array.from(arr).map(b => b.toString(16).padStart(2,'0')).join(' '); } // 字符时间与 idle 计算(经验) function charTimeMs(baud) { return 10000 / Math.max(1, baud); // ms for 1 char (10 bits) } function computeIdleMs(baud) { // Modbus RTU 标准 3.5 字符时间,但浏览器实现更粗糙,取 max(8, 3.5char) const t = Math.ceil(3.5 * charTimeMs(baud)); return Math.max(8, t); } function computePostWriteDelayMs(baud) { // 写入后等候至少 1 字符时间(加余量) return Math.max(2, Math.ceil(charTimeMs(baud))); } function sleep(ms) { return new Promise(r=>setTimeout(r, ms)); } function appendLog(text) { console.log(`[${new Date().toLocaleTimeString()}] ${text}`); } // 更新计数器显示 function updateCounters() { sendCountEl.textContent = String(sendCount); failCountEl.textContent = String(failCount); } function resetCounters() { sendCount = 0; failCount = 0; updateCounters(); } // safe writer:写并短等 async function sendRaw(data) { if (!isConnected || !port || !port.writable) throw new Error('未连接或端口不可写'); try { activeWriter = port.writable.getWriter(); await activeWriter.write(data); const postDelay = computePostWriteDelayMs(baudRate); if (postDelay > 0) await sleep(postDelay); } finally { if (activeWriter) { try { activeWriter.releaseLock(); } catch(e){} activeWriter = null; } } } // 把新字节追加到 bufferBytes function appendToBuffer(chunk) { if (!chunk || chunk.length === 0) return; const nb = new Uint8Array(bufferBytes.length + chunk.length); nb.set(bufferBytes, 0); nb.set(chunk, bufferBytes.length); bufferBytes = nb; } // 从 bufferBytes 中按 Modbus 规则提取尽可能多的完整帧,放入 framesQueue function parseBufferToFrames() { let madeProgress = false; while (bufferBytes.length >= 5) { const b0 = bufferBytes[0]; const func = bufferBytes[1]; let expectedLen = null; if (func === 3 || func === 4) { if (bufferBytes.length >= 3) { const byteCount = bufferBytes[2]; expectedLen = 3 + byteCount + 2; } else { break; } } else if (func === 6 || func === 0x10) { expectedLen = 8; } else if (func & 0x80) { expectedLen = 5; } else { expectedLen = null; } if (expectedLen !== null) { if (bufferBytes.length < expectedLen) break; const candidate = bufferBytes.slice(0, expectedLen); if (checkCRC(candidate)) { framesQueue.push({ frame: candidate, ts: Date.now() }); bufferBytes = bufferBytes.slice(expectedLen); madeProgress = true; continue; } else { let found = false; for (let i = 1; i < Math.min(16, bufferBytes.length - 4); i++) { const func2 = bufferBytes[i+1]; let exp2 = null; if (func2 === 3 || func2 === 4) { if (bufferBytes.length >= i+3) { const bc = bufferBytes[i+2]; exp2 = 3 + bc + 2; } else { exp2 = null; } } else if (func2 === 6 || func2 === 0x10) exp2 = 8; else if (func2 & 0x80) exp2 = 5; if (exp2 !== null && bufferBytes.length >= i + exp2) { const cand = bufferBytes.slice(i, i + exp2); if (checkCRC(cand)) { bufferBytes = bufferBytes.slice(i); found = true; break; } } } if (!found) { bufferBytes = bufferBytes.slice(1); } madeProgress = true; continue; } } else { break; } } return madeProgress; } function tryParseByScanning() { if (bufferBytes.length < 5) return false; for (let L = 5; L <= bufferBytes.length; L++) { const cand = bufferBytes.slice(0, L); if (checkCRC(cand)) { framesQueue.push({ frame: cand, ts: Date.now() }); bufferBytes = bufferBytes.slice(L); return true; } } for (let i = 1; i <= Math.min(32, bufferBytes.length - 5); i++) { for (let L = 5; L <= bufferBytes.length - i; L++) { const cand = bufferBytes.slice(i, i + L); if (checkCRC(cand)) { bufferBytes = bufferBytes.slice(i + L); framesQueue.push({ frame: cand, ts: Date.now() }); return true; } } } return false; } // 后台读泵主函数(长期运行) async function readPump() { if (!port || !port.readable) return; appendLog('readPump 启动'); activeReader = port.readable.getReader(); let lastReceiveTime = 0; const idleMs = computeIdleMs(baudRate); try { while (isConnected) { let result; try { result = await activeReader.read(); } catch (err) { appendLog('readPump read() 抛错: ' + (err.message || err)); break; } if (!result) break; const { value, done } = result; if (done) { appendLog('readPump: stream done'); break; } if (value && value.length) { appendLog('readPump 收到 ' + value.length + ' 字节: ' + toHex(value)); appendToBuffer(value); lastReceiveTime = Date.now(); parseBufferToFrames(); } await sleep(0); if (Date.now() - lastReceiveTime >= idleMs && bufferBytes.length >= 5) { let got = true; while (got) { got = parseBufferToFrames() || tryParseByScanning(); } } } } finally { try { await activeReader.cancel(); } catch(e) {} try { activeReader.releaseLock(); } catch(e) {} activeReader = null; appendLog('readPump 退出'); } } // 等待 framesQueue 中匹配某 (slave, funcOptions) 的响应,超时返回 null async function waitForResponse(expectedSlave, funcOptions, overallTimeoutMs = 500) { const start = Date.now(); if (!Array.isArray(funcOptions)) funcOptions = [funcOptions]; while (Date.now() - start < overallTimeoutMs) { for (let i = 0; i < framesQueue.length; i++) { const { frame } = framesQueue[i]; if (!frame || frame.length < 2) continue; if (frame[0] !== expectedSlave) continue; const f = frame[1]; if (funcOptions.includes(f)) { framesQueue.splice(i, 1); return frame; } if ((f & 0x80) && funcOptions.some(e => (e & 0x80))) { framesQueue.splice(i, 1); return frame; } } await sleep(6); } return null; } // 高级封装:发送并等待指定 func 响应(增加发送/失败计数) async function sendAndWait(frameToSend, wantFuncs, waitMs) { // 发送计数 sendCount++; updateCounters(); try { await sendRaw(frameToSend); } catch (err) { appendix: appendLog('sendAndWait 发送失败: ' + (err.message || err)); // 发送失败算作一次失败 failCount++; updateCounters(); return null; } const resp = await waitForResponse(slaveAddress, wantFuncs, waitMs); if (!resp) { appendLog('sendAndWait: 等待响应超时'); failCount++; updateCounters(); } else { appendLog('sendAndWait 收到响应: ' + toHex(resp)); } return resp; } // 解析 Modbus 0x03/0x04 响应生成寄存器数组(带 CRC 已验证) function parseReadResponseBuffer(buf, funcCode) { if (!buf || buf.length < 5) return null; if (buf[0] !== slaveAddress) return null; if (buf[1] !== funcCode) { return null; } const byteCount = buf[2]; if (buf.length < 3 + byteCount + 2) return null; const regCount = Math.floor(byteCount / 2); const regs = new Uint16Array(regCount); for (let i = 0; i < regCount; i++) { const off = 3 + i*2; regs[i] = (buf[off] << 8) | buf[off+1]; } return regs; } function parseWriteResponse(buf) { if (!buf || buf.length < 8) return false; if (buf[0] !== slaveAddress) return false; if (buf[1] !== 0x06) return false; return true; } // UI 更新 function updateDisplay() { dataDisplay.innerHTML = `<div>${lastInputData.replace(/\n/g, '<br>')}</div><div>${lastHoldingData.replace(/\n/g, '<br>')}</div>`; } function updateInputData(regs) { if (!regs) { lastInputData = '无或不完整的数据 (0x04)'; updateDisplay(); return; } let txt = '输入寄存器 (0x04):\n'; for (let i = 0; i < regs.length; i++) txt += `寄存器 ${i}: ${regs[i]}\n`; lastInputData = txt; updateDisplay(); } function updateHoldingData(regs) { if (!regs) { lastHoldingData = '无或不完整的数据 (0x03)'; updateDisplay(); return; } let txt = '保持寄存器 (0x03):\n'; for (let i = 0; i < regs.length; i++) txt += `寄存器 ${i}: ${regs[i]}\n`; lastHoldingData = txt; updateDisplay(); } // 设置待写任务 function setPendingWrite() { if (!isConnected) { appendLog('无法设置写任务:未连接'); return; } pendingWriteAddress = parseInt(document.getElementById('writeAddressInput').value) || 0; pendingWriteValue = parseInt(document.getElementById('writeValueInput').value) || 0; pendingWrite = true; writeBtn.disabled = true; appendLog(`设置待写: 地址 ${pendingWriteAddress}, 值 ${pendingWriteValue}`); } // 轮询主逻辑(串行):使用 sendAndWait 等待响应(更稳) let pollingTask = null; async function startPolling() { if (pollingTask) return; pollingTask = (async () => { appendLog('轮询开始'); while (isConnected) { try { const start = 0; const startHi = (start >> 8) & 0xFF; const startLo = start & 0xFF; const numHi = (numRegs >> 8) & 0xFF; const numLo = numRegs & 0xFF; const frame04 = buildModbusFrame(slaveAddress, 0x04, [startHi, startLo, numHi, numLo]); const resp04 = await sendAndWait(frame04, [0x04, 0x84], timeoutMs); if (resp04) { const regs = parseReadResponseBuffer(resp04, 0x04); updateInputData(regs); } else { appendLog('0x04 未收到或超时'); } const frame03 = buildModbusFrame(slaveAddress, 0x03, [startHi, startLo, numHi, numLo]); const resp03 = await sendAndWait(frame03, [0x03, 0x83], timeoutMs); if (resp03) { const regs03 = parseReadResponseBuffer(resp03, 0x03); updateHoldingData(regs03); } else { appendLog('0x03 未收到或超时'); } if (pendingWrite) { const addrHi = (pendingWriteAddress >> 8) & 0xFF; const addrLo = pendingWriteAddress & 0xFF; const valHi = (pendingWriteValue >> 8) & 0xFF; const valLo = pendingWriteValue & 0xFF; const writeFrame = buildModbusFrame(slaveAddress, 0x06, [addrHi, addrLo, valHi, valLo]); const wResp = await sendAndWait(writeFrame, [0x06, 0x86], timeoutMs); if (wResp && parseWriteResponse(wResp)) { appendLog(`写成功 地址 ${pendingWriteAddress} 值 ${pendingWriteValue}`); } else { appendLog('写保持寄存器失败或超时'); } pendingWrite = false; writeBtn.disabled = false; } } catch (err) { appendLog('轮询内部错误: ' + (err.message || err)); } await sleep(Math.max(1, intervalMs)); } appendLog('轮询结束'); pollingTask = null; })(); } // build frame function buildModbusFrame(slaveAddr, funcCode, data) { const frame = [slaveAddr, funcCode, ...data]; const crc = calculateCRC(frame); frame.push(crc & 0xFF); frame.push((crc >> 8) & 0xFF); return new Uint8Array(frame); } // 连接 / 断开 async function toggleConnection() { connectBtn.disabled = true; if (isConnected) { await closePort(); connectBtn.disabled = false; return; } baudRate = parseInt(document.getElementById('baudRateInput').value) || 115200; slaveAddress = parseInt(document.getElementById('slaveAddressInput').value) || 1; intervalMs = parseInt(document.getElementById('intervalInput').value) || 500; numRegs = parseInt(document.getElementById('numRegsInput').value) || 10; timeoutMs = parseInt(document.getElementById('timeoutInput').value) || 500; try { port = await navigator.serial.requestPort({ filters: [] }); await port.open({ baudRate: baudRate, dataBits: 8, stopBits: 1, parity: 'none', bufferSize: 1024, flowControl: 'none' }); isConnected = true; connectBtn.textContent = '断开连接'; connectBtn.classList.add('connected'); connInfo.textContent = `已连接: ${baudRate}-8-N-1, 从机地址: ${slaveAddress}`; appendLog('串口已打开'); writeBtn.disabled = false; // 启动读泵 readPumpTask = readPump(); // 启动轮询 startPolling(); } catch (err) { appendLog('连接失败: ' + (err.message || err)); alert('连接串口失败: ' + (err.message || err)); isConnected = false; connectBtn.textContent = '连接设备'; connectBtn.classList.remove('connected'); } finally { connectBtn.disabled = false; } } // 关闭端口(安全) async function closePort() { appendLog('开始断开'); try { isConnected = false; if (activeReader) { try { await activeReader.cancel(); } catch(e) {} try { activeReader.releaseLock(); } catch(e) {} activeReader = null; } const giveUp = Date.now() + 400; while (readPumpTask && Date.now() < giveUp) { await sleep(20); } if (port) { try { await port.close(); } catch(e) { appendLog('port.close 错: ' + (e.message||e)); } port = null; } } finally { isConnected = false; bufferBytes = new Uint8Array(0); framesQueue = []; pendingWrite = false; connectBtn.textContent = '连接设备'; connectBtn.classList.remove('connected'); connInfo.textContent = ''; lastInputData = '无数据'; lastHoldingData = '无数据'; updateDisplay(); writeBtn.disabled = true; appendLog('断开完成'); } } // 页面卸载时确保断开 window.addEventListener('beforeunload', async (e) => { if (isConnected) { try { await closePort(); } catch(e) {} } }); // init function resetDisplay() { lastInputData = '无数据'; lastHoldingData = '无数据'; updateDisplay(); } resetDisplay(); updateCounters(); window.toggleConnection = toggleConnection; window.setPendingWrite = setPendingWrite; window.resetCounters = resetCounters; </script> </body> </html>