使用JavaScript处理串口数据(Modbus协议)
一:接收任意数量的输入寄存器数据并生成折线图

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modbus Serial Tool</title> <!-- Replace the Chart.js CDN with this line --> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> <style> :root { --primary-color: #2196F3; --success-color: #4CAF50; --error-color: #f44336; --text-dark: #2c3e50; --background-light: #f8f9fa; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: var(--background-light); color: var(--text-dark); } h1 { color: var(--primary-color); text-align: center; margin-bottom: 2rem; } .toolbar { display: flex; gap: 1rem; margin-bottom: 1.5rem; } button { padding: 0.8rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; } button:disabled { opacity: 0.6; cursor: not-allowed; } #open-port { background-color: var(--success-color); color: white; } #close-port { background-color: var(--error-color); color: white; } .status { padding: 1rem; border-radius: 4px; margin: 1rem 0; transition: all 0.3s ease; } .connected { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; } .disconnected { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } .modbus-section { background: white; border-radius: 8px; padding: 1.5rem; margin: 1.5rem 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .input-group { display: flex; align-items: center; gap: 1rem; margin: 1rem 0; } label { width: 120px; font-weight: 500; } input[type="number"] { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 150px; transition: border-color 0.3s ease; } input[type="number"]:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(33,150,243,0.2); } #read-registers { background-color: var(--primary-color); color: white; margin-top: 1rem; } #myChart { width: 100% !important; height: 300px !important; margin-top: 1rem; } #response-data { background: white; padding: 1rem; border-radius: 4px; border: 1px solid #eee; min-height: 100px; overflow-x: auto; } @media (max-width: 768px) { .modbus-section { flex-direction: column; } #myChart { margin-top: 2rem; } .input-group { flex-direction: column; align-items: flex-start; } } </style> </head> <body> <!-- 保持原有的HTML结构不变,仅修改class --> <h1>Modbus RTU Reader</h1> <div class="toolbar"> <button id="open-port">Connect</button> <button id="close-port" disabled>Disconnect</button> </div> <div id="port-status" class="status"></div> <div class="modbus-section"> <div> <h3>Read Input Registers</h3> <div class="input-group"> <label>Slave Address:</label> <input type="number" id="slave-address" min="1" max="247" value="1"> </div> <div class="input-group"> <label>Start Address:</label> <input type="number" id="start-address" min="0" value="0"> </div> <div class="input-group"> <label>Quantity:</label> <input type="number" id="quantity" min="1" max="125" value="125"> </div> <button id="read-registers" disabled>Read Registers</button> </div> <canvas id="myChart"></canvas> </div> <div class="modbus-section"> <h3>Results</h3> <pre id="response-data"></pre> </div> <script> let port, reader, writer; // CRC16计算函数(Modbus RTU) function crc16(data) { const auchCRCHi = [ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 ]; const auchCRCLo = [ 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 ]; let crc_hi = 0xFF; let crc_lo = 0xFF; for (let i = 0; i < data.length; i++) { const idx = crc_hi ^ data[i]; crc_hi = crc_lo ^ auchCRCHi[idx]; crc_lo = auchCRCLo[idx]; } return new Uint8Array([crc_hi, crc_lo]); } // 构建Modbus请求帧 function buildModbusFrame(slaveId, functionCode, startAddress, quantity) { const buffer = new ArrayBuffer(6); const view = new DataView(buffer); view.setUint8(0, slaveId); // 从机地址 view.setUint8(1, functionCode); // 功能码04 view.setUint16(2, startAddress, false); // 起始地址 view.setUint16(4, quantity, false); // 寄存器数量 const data = new Uint8Array(buffer); const crc = crc16(data); // CRC now returns a Uint8Array with 2 bytes // Create new array for data + CRC const result = new Uint8Array(data.length + crc.length); // Copy data to result array result.set(data, 0); // Copy CRC to result array result.set(crc, data.length); return result; } // 解析Modbus响应(带CRC校验) function parseModbusResponse(response, slaveId) { // 合并所有chunk为一个Uint8Array const merged = new Uint8Array(response.reduce((acc, curr) => [...acc, ...curr], [])); // 检查最小长度(从机地址1 + 功能码1 + 字节数1 + 数据0 + CRC2) if (merged.length < 5) return null; if (merged[0] != slaveId) return null; // 分离数据和CRC(最后2字节为CRC) const dataPart = merged.slice(0, -2); const receivedCRC = merged.slice(-2); // 计算数据的CRC const calculatedCRC = crc16(dataPart); // 校验CRC是否匹配(需考虑字节顺序) if (calculatedCRC[0] !== receivedCRC[0] || calculatedCRC[1] !== receivedCRC[1]) { return null; } // 解析数据部分 const dataView = new DataView(dataPart.buffer); const byteCount = dataView.getUint8(2); // 检查数据长度是否匹配 if (dataPart.length !== 3 + byteCount) { return null; } const registers = []; for (let i = 0; i < byteCount/2; i++) { registers.push(dataView.getUint16(3 + i*2, false)); } return { slaveId: dataView.getUint8(0), functionCode: dataView.getUint8(1), registers: registers }; } // 在全局添加读取状态锁 let isReading = false; // 读取输入寄存器 async function readInputRegisters() { // 添加互斥锁防止重复点击 if (isReading) return; isReading = true; const readButton = document.getElementById('read-registers'); readButton.disabled = true; // 禁用按钮防止重复点击 try { const slaveId = parseInt(document.getElementById('slave-address').value); const startAddr = parseInt(document.getElementById('start-address').value); const quantity = parseInt(document.getElementById('quantity').value); // 构建请求帧 const request = buildModbusFrame(slaveId, 0x04, startAddr, quantity); // 发送请求 writer = port.writable.getWriter(); await writer.write(request); await writer.close(); // 关闭并释放writer writer = null; // 确保之前的reader已释放 if (reader) { await reader.releaseLock(); reader = null; } // 读取响应 reader = port.readable.getReader(); // 此时流未被锁定,可以安全获取 const response = await readWithTimeout(reader, 50); const result = parseModbusResponse(response, slaveId); if(result != null){ // 显示结果 将 registers 中的所有数据用空格连接并打印在同一行 document.getElementById('response-data').textContent = JSON.stringify(result.registers.join(' '), null, 2); //更新折线图 createLineChart(result.registers); } } catch (error) { console.error("Modbus error:", error); alert(`Error: ${error.message}`); } finally { // 确保释放所有锁 if (writer) { await writer.releaseLock(); writer = null; } if (reader) { await reader.releaseLock(); reader = null; } isReading = false; readButton.disabled = false; // 重新启用按钮 } } // 定义一个全局变量来存储图表实例 let myChart; function createLineChart(registers) { var ctx = document.getElementById('myChart').getContext('2d'); // 如果已经存在图表实例,销毁它 if (myChart) { myChart.destroy(); } // 创建新的图表实例 myChart = new Chart(ctx, { type: 'line', // 图表类型为折线图 data: { labels: registers.map((_, index) => index), // 用 registers 数据的索引作为 X 轴标签 datasets: [{ label: 'AB电压差', // 标签 data: registers, // 用 registers 数据作为 Y 轴数据 borderColor: 'rgba(75, 192, 192, 1)', // 折线颜色 backgroundColor: 'rgba(75, 192, 192, 0.2)', // 填充颜色 borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true // Y轴从0开始 } } } }); } async function readWithTimeout(reader, timeout) { const chunks = []; try { // 读取第一个数据块,不设置超时以确保至少开始接收一帧 let firstResult = await reader.read(); if (firstResult.done) return chunks; // 如果流已结束直接返回 if (firstResult.value) { chunks.push(firstResult.value); console.log("Received first chunk:", firstResult.value); } // 后续读取使用超时机制 while (true) { // 创建带超时的读取任务 const timer = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout waiting for response")), timeout) ); // 同时等待读取结果和超时 const { value, done } = await Promise.race([reader.read(), timer]); // 如果触发的是超时,直接返回已收集的数据 if (!done && !value) break; // 此处通过race机制自动处理 console.log("Received value:", value, "done:", done); if (done) break; // 流正常结束 if (value) chunks.push(value); // 收集有效数据 } return chunks; } catch (error) { // 超时后返回已收集的数据(至少有一个初始数据块) if (error.message === "Timeout waiting for response") { return chunks; } // 其他错误处理 alert(`接收失败: ${error.message}`); throw error; } } // 更新状态显示 function updateStatus(isConnected, portPath = "") { const statusElement = document.getElementById('port-status'); if (isConnected) { statusElement.textContent = `已连接: ${portPath}`; statusElement.className = "status connected"; } else { statusElement.textContent = "串口未连接"; statusElement.className = "status disconnected"; } } // 打开串口 async function openPort() { try { port = await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); // 更新UI状态 document.getElementById('open-port').disabled = true; document.getElementById('close-port').disabled = false; document.getElementById('read-registers').disabled = false; updateStatus(true, port.path); // 删除此处多余的 reader 初始化 } catch (error) { console.error("Error opening port:", error); if (error.name === 'NotFoundError') { alert('没有选择串口设备!'); } updateStatus(false); } } // 关闭串口 async function closePort() { try { if (reader) { await reader.cancel(); reader = null; } if (writer) { await writer.close(); writer = null; } if (port) { await port.close(); port = null; } // 更新UI状态 document.getElementById('open-port').disabled = false; document.getElementById('close-port').disabled = true; document.getElementById('read-registers').disabled = true; updateStatus(false); } catch (error) { console.error("Error closing port:", error); } } // 在openPort函数最后添加: document.getElementById('read-registers').disabled = false; // 在closePort函数中添加: document.getElementById('read-registers').disabled = true; // 事件监听 document.getElementById('read-registers').addEventListener('click', readInputRegisters); document.getElementById('open-port').addEventListener('click', openPort); document.getElementById('close-port').addEventListener('click', closePort); </script> </body> </html>
使用Edge浏览器打开,效果如下
二:实时发送保持寄存器和更新输入寄存器

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modbus Serial Tool</title> <style> /* 定义一些全局的 CSS 变量,方便统一管理颜色 */ :root { --primary-color: #2196F3; /* 主色调 */ --success-color: #4CAF50; /* 成功色调 */ --error-color: #f44336; /* 错误色调 */ --text-dark: #2c3e50; /* 深色文本 */ --background-light: #f8f9fa; /* 背景浅色 */ } /* 设置全局样式 */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 字体 */ max-width: 1200px; /* 最大宽度 */ margin: 0 auto; /* 居中 */ padding: 20px; /* 内边距 */ background-color: var(--background-light); /* 背景颜色 */ color: var(--text-dark); /* 文本颜色 */ } /* 标题样式 */ h1 { color: var(--primary-color); /* 主色调 */ text-align: center; /* 居中 */ margin-bottom: 2rem; /* 底部外边距 */ } /* 工具栏样式 */ .toolbar { display: flex; /* 弹性布局 */ gap: 1rem; /* 元素间距 */ margin-bottom: 1.5rem; /* 底部外边距 */ } /* 按钮样式 */ button { padding: 0.5rem; /* 内边距 */ border: 1px solid #ddd; /* 边框 */ border-radius: 4px; /* 圆角 */ cursor: pointer; /* 鼠标指针 */ transition: all 0.3s ease; /* 过渡效果 */ width: 100px; /* 宽度 */ } /* 禁用按钮样式 */ button:disabled { opacity: 0.6; /* 透明度 */ cursor: not-allowed; /* 禁用鼠标指针 */ } /* 打开串口按钮样式 */ #open-port { background-color: var(--success-color); /* 成功色调 */ color: white; /* 白色文本 */ } /* 关闭串口按钮样式 */ #close-port { background-color: var(--error-color); /* 错误色调 */ color: white; /* 白色文本 */ } /* 状态显示区域样式 */ .status { padding: 1rem; /* 内边距 */ border-radius: 4px; /* 圆角 */ margin: 1rem 0; /* 外边距 */ transition: all 0.3s ease; /* 过渡效果 */ } /* 连接成功状态样式 */ .connected { background-color: #d4edda; /* 背景颜色 */ border: 1px solid #c3e6cb; /* 边框 */ color: #155724; /* 文本颜色 */ } /* 断开连接状态样式 */ .disconnected { background-color: #f8d7da; /* 背景颜色 */ border: 1px solid #f5c6cb; /* 边框 */ color: #721c24; /* 文本颜色 */ } /* Modbus 功能区域样式 */ .modbus-section { background: white; /* 白色背景 */ border-radius: 8px; /* 圆角 */ padding: 1.5rem; /* 内边距 */ margin: 1.5rem 0; /* 外边距 */ box-shadow: 0 2px 8px rgba(0,0,0,0.1); /* 阴影 */ } /* 输入组样式 */ .input-group { display: flex; /* 弹性布局 */ align-items: center; /* 垂直居中 */ gap: 1rem; /* 元素间距 */ margin: 1rem 0; /* 外边距 */ } .position-display { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; margin-top: 1rem; } .axis-card { background: white; border-radius: 8px; padding: 1.2rem; box-shadow: 0 2px 12px rgba(0,0,0,0.08); transition: transform 0.2s ease; } .axis-card:hover { transform: translateY(-2px); } .axis-header { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.8rem; } .axis-icon { font-size: 1.8rem; width: 40px; height: 40px; background: var(--primary-color); color: white; border-radius: 6px; display: flex; align-items: center; justify-content: center; } .axis-title { font-weight: 600; color: var(--text-dark); font-size: 1.1rem; } .axis-value { font-size: 1.8rem; font-weight: 700; color: var(--primary-color); text-align: center; font-family: 'Courier New', monospace; padding: 0.5rem; background: #f8f9fa; border-radius: 6px; } .control-card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 3px 15px rgba(0,0,0,0.1); transition: transform 0.2s ease; } .control-card:hover { transform: translateY(-2px); } .control-group { display: flex; flex-direction: column; gap: 1.2rem; } .input-wrapper { display: flex; flex-direction: column; gap: 0.5rem; } .input-wrapper label { font-size: 0.9rem; color: #666; font-weight: 500; } .modern-input { padding: 0.8rem; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 1.1rem; transition: all 0.3s ease; width: 100%; } .modern-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(33,150,243,0.15); outline: none; } .button-group { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.8rem; } .control-btn { padding: 0.8rem 1rem; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .control-btn.absolute { background: var(--primary-color); color: white; grid-column: span 3; } .control-btn.relative { background: #e3f2fd; color: var(--primary-color); border: 1px solid var(--primary-color); } .control-btn.relative:hover { background: var(--primary-color); color: white; } .control-btn:hover { filter: brightness(0.95); transform: translateY(-1px); } @media (max-width: 768px) { .button-group { grid-template-columns: 1fr; } .control-btn.absolute { grid-column: span 1; } .modern-input { font-size: 1rem; } } @media (max-width: 768px) { .position-display { grid-template-columns: 1fr; } .axis-value { font-size: 1.5rem; } } /* 标签样式 */ label { width: 120px; /* 宽度 */ font-weight: 500; /* 字体加粗 */ } /* 数字输入框样式 */ input[type="number"] { padding: 0.5rem; /* 内边距 */ border: 1px solid #ddd; /* 边框 */ border-radius: 4px; /* 圆角 */ width: 150px; /* 宽度 */ transition: border-color 0.3s ease; /* 过渡效果 */ } /* 数字输入框聚焦样式 */ input[type="number"]:focus { outline: none; /* 去除轮廓 */ border-color: var(--primary-color); /* 主色调边框 */ box-shadow: 0 0 0 2px rgba(33,150,243,0.2); /* 阴影 */ } /* 读取寄存器按钮样式 */ #read-registers { background-color: var(--primary-color); /* 主色调 */ color: white; /* 白色文本 */ margin-top: 1rem; /* 顶部外边距 */ } /* 图表样式 */ #myChart { width: 100% !important; /* 宽度100% */ height: 300px !important; /* 高度300px */ margin-top: 1rem; /* 顶部外边距 */ } /* 响应数据区域样式 */ #response-data { font-size: 16px; /* 字体大小 */ padding: 0px; /* 内边距 */ background-color: hsl(103, 47%, 78%); /* 背景颜色 */ border: 1px solid #ddd; /* 边框 */ border-radius: 8px; /* 圆角 */ box-shadow: 0 2px 8px rgba(0,0,0,0.1); /* 阴影 */ margin-top: 1rem; /* 顶部外边距 */ } /* 响应内容样式 */ #response-content { display: block; /* 确保内容占满一行 */ font-family: 'Courier New', Courier, monospace; /* 使用等宽字体 */ color: #333; /* 文本颜色 */ line-height: 1.6; /* 行高 */ } /* 响应式设计,屏幕宽度小于768px时的样式 */ @media (max-width: 768px) { .modbus-section { flex-direction: column; /* 垂直排列 */ } #myChart { margin-top: 2rem; /* 顶部外边距 */ } .input-group { flex-direction: column; /* 垂直排列 */ align-items: flex-start; /* 左对齐 */ } } /* 寄存器修改区域样式 */ .register-edit { margin-top: 1rem; /* 顶部外边距 */ display: flex; /* 弹性布局 */ gap: 1rem; /* 元素间距 */ align-items: center; /* 垂直居中 */ } #register-value { width: 100px; /* 宽度 */ } </style> </head> <body> <!-- 页面标题 --> <h1>Modbus RTU Tool</h1> <!-- 工具栏,包含连接和断开按钮 --> <div class="toolbar"> <button id="open-port">Connect</button> <button id="close-port" disabled>Disconnect</button> </div> <!-- 串口连接状态显示区域 --> <div id="port-status" class="status"></div> <!-- Modbus 读取输入寄存器区域 --> <div class="modbus-section"> <h3>输入寄存器读取</h3> <div class="position-display"> <!-- X轴位置 --> <div class="axis-card"> <div class="axis-header"> <span class="axis-icon">↔</span> <span class="axis-title">X轴位置</span> </div> <div class="axis-value" id="current-pos-x">0.0000 mm</div> </div> <!-- Y轴位置 --> <div class="axis-card"> <div class="axis-header"> <span class="axis-icon">↕</span> <span class="axis-title">Y轴位置</span> </div> <div class="axis-value" id="current-pos-y">0.0000 mm</div> </div> <!-- Z轴位置 --> <div class="axis-card"> <div class="axis-header"> <span class="axis-icon">⤒</span> <span class="axis-title">Z轴位置</span> </div> <div class="axis-value" id="current-pos-z">0.0000 mm</div> </div> </div> </div> <!-- 保持寄存器控制区域修改后代码 --> <div class="modbus-section"> <h3>轴控制面板</h3> <div class="position-display"> <!-- X轴控制 --> <div class="control-card"> <div class="axis-header"> <span class="axis-icon">↔</span> <span class="axis-title">X轴控制</span> </div> <div class="control-group"> <div class="input-wrapper"> <label>目标位置 (mm)</label> <input type="number" id="x-axis-value" value="0.0001" max="38.0" min="-20.0" step="0.1" class="modern-input"> </div> <div class="button-group"> <button class="control-btn absolute" data-axis-move="x-absolute">绝对定位</button> <button class="control-btn relative" data-axis-move="x-relativePlus">+ 增量</button> <button class="control-btn relative" data-axis-move="x-relativeMinus">- 增量</button> </div> </div> </div> <!-- Y轴控制 --> <div class="control-card"> <div class="axis-header"> <span class="axis-icon">↕</span> <span class="axis-title">Y轴控制</span> </div> <div class="control-group"> <div class="input-wrapper"> <label>目标位置 (mm)</label> <input type="number" id="y-axis-value" value="1.0" max="38.0" min="-20.0" step="0.1" class="modern-input"> </div> <div class="button-group"> <button class="control-btn absolute" data-axis-move="y-absolute">绝对定位</button> <button class="control-btn relative" data-axis-move="y-relativePlus">+ 增量</button> <button class="control-btn relative" data-axis-move="y-relativeMinus">- 增量</button> </div> </div> </div> <!-- Z轴控制 --> <div class="control-card"> <div class="axis-header"> <span class="axis-icon">⤒</span> <span class="axis-title">Z轴控制</span> </div> <div class="control-group"> <div class="input-wrapper"> <label>目标位置 (mm)</label> <input type="number" id="z-axis-value" value="1.0" max="38.0" min="-20.0" step="0.1" class="modern-input"> </div> <div class="button-group"> <button class="control-btn absolute" data-axis-move="z-absolute">绝对定位</button> <button class="control-btn relative" data-axis-move="z-relativePlus">+ 增量</button> <button class="control-btn relative" data-axis-move="z-relativeMinus">- 增量</button> </div> </div> </div> </div> </div> <script> let port, reader, writer; let isRunning = false; // 添加运行状态变量 let intervalId = null; // 添加循环ID变量 // 在全局添加保持寄存器相关变量 let holdingRegisters = new Array(100).fill(0); let sendIntervalId = null; let isSending = false; // CRC16计算函数(Modbus RTU) function crc16(data) { const auchCRCHi = [ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 ]; const auchCRCLo = [ 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 ]; let crc_hi = 0xFF; let crc_lo = 0xFF; for (let i = 0; i < data.length; i++) { const idx = crc_hi ^ data[i]; crc_hi = crc_lo ^ auchCRCHi[idx]; crc_lo = auchCRCLo[idx]; } return new Uint8Array([crc_hi, crc_lo]); } // 构建写保持寄存器请求帧(功能码0x10) function buildWriteHoldingFrame(slaveId, startAddress, registers) { const byteCount = registers.length * 2; const buffer = new ArrayBuffer(7 + byteCount); const view = new DataView(buffer); view.setUint8(0, slaveId); // 从机地址 view.setUint8(1, 0x10); // 功能码 view.setUint16(2, startAddress, false); // 起始地址 view.setUint16(4, registers.length, false); // 寄存器数量 view.setUint8(6, byteCount); // 字节数 // 填充寄存器数据 let offset = 7; registers.forEach(value => { view.setUint16(offset, value, false); offset += 2; }); const data = new Uint8Array(buffer); const crc = crc16(data); const result = new Uint8Array(data.length + crc.length); result.set(data); result.set(crc, data.length); return result; } function floatToRegisters(value) { const buffer = new ArrayBuffer(4); new DataView(buffer).setFloat32(0, value, false); // 大端序 return [ new DataView(buffer).getUint16(2), // CD部分 new DataView(buffer).getUint16(0) // AB部分 ]; } function registersToFloat(cd, ab) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint16(0, ab); // AB部分 view.setUint16(2, cd); // CD部分 return view.getFloat32(0, false); // 大端序 } // 宏定义的寄存器地址 const Addr_Ho_Kp_Speed = 0; const Addr_Ho_Ki_Speed = 2; const Addr_Ho_Kp_Uq = 4; const Addr_Ho_Ki_Uq = 6; const Addr_Ho_MaxAngularVelocity = 8; const Addr_Ho_SetUq = 15; const Addr_Ho_VcmKp = 20; const Addr_Ho_VcmKi = 22; const Addr_Ho_VcmSpeedMax = 26; const Addr_Ho_VcmSpeedAcc = 28; const Addr_Ho_FocusOffSet = 30; const Addr_Ho_W0 = 31; const Addr_Ho_W1 = 32; const Addr_Ho_RedLaserPower = 33; const Addr_Ho_GreenLaserPower = 34; const Addr_Ho_HandleLaser = 35; const Addr_Ho_HandleFocus = 36; const Addr_Ho_KpFocus = 38; const Addr_Ho_SetPosX = 40; const Addr_Ho_HandleX = 42; const Addr_Ho_SetPosY = 43; const Addr_Ho_HandleY = 45; const Addr_Ho_SetPosZ = 46; const Addr_Ho_HandleZ = 48; const Addr_In_CurPosX = 40; const Addr_In_CurPosY = 42; const Addr_In_CurPosZ = 44; // 初始化Modbus数据 function initModbus() { // 定义需要初始化的寄存器地址和对应的值 const initValues = [ { address: Addr_Ho_Kp_Speed, value: 0.1, type: 'float' }, // Kp_Speed { address: Addr_Ho_Ki_Speed, value: 0.0, type: 'float' }, // Ki_Speed { address: Addr_Ho_Kp_Uq, value: 10.0, type: 'float' }, // Kp_Uq { address: Addr_Ho_Ki_Uq, value: 0.2, type: 'float' }, // Ki_Uq { address: Addr_Ho_MaxAngularVelocity, value: 50, type: 'float' }, // MaxAngularVelocity { address: Addr_Ho_SetUq, value: 12, type: 'float' }, { address: Addr_Ho_VcmKp, value: 30, type: 'float' }, { address: Addr_Ho_VcmKi, value: 1, type: 'float' }, { address: Addr_Ho_VcmSpeedMax, value: 200, type: 'float' }, { address: Addr_Ho_VcmSpeedAcc, value: 80, type: 'float' }, { address: Addr_Ho_W0, value: 55, type: 'int' }, { address: Addr_Ho_W1, value: 128, type: 'int' }, ]; // 遍历初始化值并写入寄存器 initValues.forEach(({ address, value, type }) => { if (address < 0 || address >= holdingRegisters.length) { console.error(`地址 ${address} 超出范围`); return; } if (type === 'float') { // 浮点数需要占用两个寄存器 if (address + 1 >= holdingRegisters.length) { console.error(`地址 ${address} 超出范围(浮点数需要两个寄存器)`); return; } // 将浮点数转换为寄存器值 const [cd, ab] = floatToRegisters(value); if (cd === undefined || ab === undefined) { console.error(`值 ${value} 转换失败`); return; } // 写入寄存器 holdingRegisters[address + 1] = ab; // AB 部分 holdingRegisters[address + 0] = cd; // CD 部分 } else if (type === 'int') { // 整数直接写入 holdingRegisters[address] = value; } else { console.error(`未知的类型: ${type}`); } }); console.log("初始化寄存器完成"); } // 定义各轴的寄存器偏移配置 const axisConfig = { x: { valueAddr: Addr_Ho_SetPosX, // 值存储起始地址 controlAddr: Addr_Ho_HandleX, // 控制指令地址 inputId: 'x-axis-value' }, y: { valueAddr: Addr_Ho_SetPosY, controlAddr: Addr_Ho_HandleY, inputId: 'y-axis-value' }, z: { valueAddr: Addr_Ho_SetPosZ, controlAddr: Addr_Ho_HandleZ, inputId: 'z-axis-value' } }; // 统一处理轴移动 function handleAxisMove(axisType, moveType) { const config = axisConfig[axisType]; if (!config) return; // 获取输入值 const value = parseFloat(document.getElementById(config.inputId).value); // 转换为寄存器值并更新 const [cd, ab] = floatToRegisters(value); holdingRegisters[config.valueAddr + 1] = ab; holdingRegisters[config.valueAddr + 0] = cd; // 设置控制指令 holdingRegisters[config.controlAddr] = moveType; } // 事件监听统一处理(替换原来的多个事件监听) document.querySelectorAll('[data-axis-move]').forEach(button => { button.addEventListener('click', (e) => { const [axis, type] = e.target.dataset.axisMove.split('-'); const moveType = { absolute: 1, relativePlus: 2, relativeMinus: 3 }[type]; if (moveType) handleAxisMove(axis, moveType); }); }); // 构建Modbus请求帧 function buildModbusFrame(slaveId, functionCode, startAddress, quantity) { const buffer = new ArrayBuffer(6); const view = new DataView(buffer); view.setUint8(0, slaveId); // 从机地址 view.setUint8(1, functionCode); // 功能码04 view.setUint16(2, startAddress, false); // 起始地址 view.setUint16(4, quantity, false); // 寄存器数量 const data = new Uint8Array(buffer); const crc = crc16(data); // CRC now returns a Uint8Array with 2 bytes // Create new array for data + CRC const result = new Uint8Array(data.length + crc.length); // Copy data to result array result.set(data, 0); // Copy CRC to result array result.set(crc, data.length); return result; } // 解析Modbus响应(带CRC校验) function parseModbusResponse(response, slaveId) { // 合并所有chunk为一个Uint8Array const merged = new Uint8Array(response.reduce((acc, curr) => [...acc, ...curr], [])); // 检查最小长度(从机地址1 + 功能码1 + 字节数1 + 数据0 + CRC2) if (merged.length < 5) return null; if (merged[0] != slaveId) return null; // 分离数据和CRC(最后2字节为CRC) const dataPart = merged.slice(0, -2); const receivedCRC = merged.slice(-2); // 计算数据的CRC const calculatedCRC = crc16(dataPart); // 校验CRC是否匹配(需考虑字节顺序) if (calculatedCRC[0] !== receivedCRC[0] || calculatedCRC[1] !== receivedCRC[1]) { return null; } // 解析数据部分 const dataView = new DataView(dataPart.buffer); const byteCount = dataView.getUint8(2); // 检查数据长度是否匹配 if (dataPart.length !== 3 + byteCount) { return null; } const registers = []; for (let i = 0; i < byteCount/2; i++) { registers.push(dataView.getUint16(3 + i*2, false)); } return { slaveId: dataView.getUint8(0), functionCode: dataView.getUint8(1), registers: registers }; } async function readWithTimeout(reader, timeout) { const chunks = []; try { // 读取第一个数据块,不设置超时以确保至少开始接收一帧 let firstResult = await reader.read(); if (firstResult.done) return chunks; // 如果流已结束直接返回 if (firstResult.value) { chunks.push(firstResult.value); console.log("Received first chunk:", firstResult.value); } // 后续读取使用超时机制 while (true) { // 创建带超时的读取任务 const timer = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout waiting for response")), timeout) ); // 同时等待读取结果和超时 const { value, done } = await Promise.race([reader.read(), timer]); // // 如果触发的是超时,直接返回已收集的数据 // if (!done && !value) break; // 此处通过race机制自动处理 // console.log("Received value:", value, "done:", done); if (done) break; // 流正常结束 if (value) chunks.push(value); // 收集有效数据 } return chunks; } catch (error) { // 超时后返回已收集的数据(至少有一个初始数据块) if (error.message === "Timeout waiting for response") { return chunks; } // 其他错误处理 alert(`接收失败: ${error.message}`); throw error; } } // 更新状态显示 function updateStatus(isConnected, portPath = "") { const statusElement = document.getElementById('port-status'); if (isConnected) { statusElement.textContent = `已连接: ${portPath}`; statusElement.className = "status connected"; } else { statusElement.textContent = "串口未连接"; statusElement.className = "status disconnected"; } } // 打开串口并启动循环 async function openPort() { try { port = await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); // 更新UI状态 document.getElementById('open-port').disabled = true; document.getElementById('close-port').disabled = false; updateStatus(true, port.path); // 启动循环任务 isRunning = true; runLoop(); } catch (error) { console.error("Error opening port:", error); updateStatus(false); } } // 关闭串口并停止循环 async function closePort() { try { // 停止循环 isRunning = false; if (intervalId) { clearInterval(intervalId); intervalId = null; } // 关闭串口资源 if (reader) { await reader.cancel(); await reader.releaseLock(); reader = null; // 重置 reader } if (writer) { try { await writer.close(); // 尝试关闭 writer } catch (error) { console.error("Error closing writer:", error); } await writer.releaseLock(); writer = null; // 重置 writer } if (port) await port.close(); // 强制更新UI状态 document.getElementById('open-port').disabled = false; document.getElementById('close-port').disabled = true; updateStatus(false); } catch (error) { console.error("Error closing port:", error); } } // 修改后的主循环函数 async function runLoop() { if (!isRunning) return; try { // 使用setInterval代替requestAnimationFrame intervalId = setInterval(async () => { if (!isRunning) { clearInterval(intervalId); return; } // 发送保持寄存器 await sendHoldingRegisters(); await delay(50); // 读取输入寄存器 await readInputRegisters(); await delay(50); }, 100); } catch (error) { console.error("循环任务出错:", error); closePort(); } } // 延时函数 function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 发送保持寄存器 async function sendHoldingRegisters() { if (!port?.writable) return; try { const slaveId = 1; // 假设固定从机地址 const snapshot = [...holdingRegisters]; const request = buildWriteHoldingFrame(slaveId, 0, snapshot); writer = port.writable.getWriter(); await writer.write(request); await writer.releaseLock(); // 重置控制位 holdingRegisters[Addr_Ho_HandleX] = 0; holdingRegisters[Addr_Ho_HandleY] = 0; holdingRegisters[Addr_Ho_HandleZ] = 0; } catch (error) { console.error("发送失败:", error); throw error; // 抛出错误以便主循环处理 } } async function clearBuffer(reader) { try { while (true) { const { value, done } = await reader.read(); if (done) break; // 流结束 if (!value) break; // 没有数据 } } catch (error) { console.error("清空缓冲区失败:", error); } } // 读取输入寄存器 async function readInputRegisters() { let reader = null; // 显式声明 reader if (!port?.readable) return; try { const slaveId = 1; const request = buildModbusFrame(slaveId, 0x04, 0, 100); // 清空缓冲区 reader = port.readable.getReader(); // 明确初始化 reader await reader.cancel(); await reader.releaseLock(); // 发送请求 const writer = port.writable.getWriter(); await writer.write(request); await writer.releaseLock(); // 读取响应 reader = port.readable.getReader(); // 重新初始化 reader const response = await readWithTimeout(reader, 30); const result = parseModbusResponse(response, slaveId); if (result) { // 提取 XYZ 位置并更新显示 const curPosX = registersToFloat(result.registers[Addr_In_CurPosX], result.registers[Addr_In_CurPosX + 1]); const curPosY = registersToFloat(result.registers[Addr_In_CurPosY], result.registers[Addr_In_CurPosY + 1]); const curPosZ = registersToFloat(result.registers[Addr_In_CurPosZ], result.registers[Addr_In_CurPosZ + 1]); // 更新显示 document.getElementById('current-pos-x').textContent = `${curPosX.toFixed(4)} mm`; document.getElementById('current-pos-y').textContent = `${curPosY.toFixed(4)} mm`; document.getElementById('current-pos-z').textContent = `${curPosZ.toFixed(4)} mm`; } } catch (error) { console.error("读取失败:", error); throw error; } finally { if (reader) { try { await reader.releaseLock(); // 确保释放锁 } catch (releaseError) { console.error("释放 reader 锁失败:", releaseError); } } } } document.getElementById('open-port').addEventListener('click', openPort); document.getElementById('close-port').addEventListener('click', closePort); // 初始化保持寄存器默认值 initModbus(); </script> </body> </html>
效果如下
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2020-02-18 Halcon 形态学膨胀腐蚀理论知识学习笔记