使用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>
View Code
复制代码

使用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>
View Code
复制代码

效果如下

 

posted @   阿坦  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
历史上的今天:
2020-02-18 Halcon 形态学膨胀腐蚀理论知识学习笔记
点击右上角即可分享
微信分享提示