自制RS485 调试工具

前言:手里恰好有一个空余的ESP32。想着可以做一个RS485串口调试工具。

资源:esp32读取RS485串口小工具

功能

1.esp32在AP模式下,手机连接上esp32的wifi。
2.用手机浏览器实现在线调试RS485.

硬件要求

1.esp32,大概17块。(便宜点可以用esp8266代替,淘宝10块左右)
2.MAX485模块,大概2块左右。功能为ttl转RS485。优点便宜,2块左右。缺点是无法自动流控。可以选自动流控的模块,大概10块左右。
3.电源。可以用充电宝。
4.不要钱的信号线。
5.micro USB线(带有通讯功能)用于调试。

步骤

软件部分

  1. 电脑下载安装好Thonny,官网下载源为github,十分缓慢。可以点此下载。电脑安装好CH340的驱动,点此下载
    2.将固件刷成micropython。
    3.打开安装好的Thonny软件。点击运行——配置解释器——解释器。在界面中选择哪种解释器,micropython(esp32)。本次使用的是esp32.在端口选择esp32的com口。然后点击安装或者更新micropython。按要求下载,等待下载完成。
    4.在Tonny的视图,把文件勾选上。然后新建一个main.py文件。然后把程序输入:
import network
import socket
from machine import UART, Pin
import time
import gc
import json
# 全局变量
rs485_data = []
max_data_length = 1000
current_baudrate = 4800
display_format = "ascii"
uart_instance = None  # 全局UART实例
# 配置AP模式(开放网络)
def setup_ap():
    ap = network.WLAN(network.AP_IF)
    ap.active(True)
    # 创建开放网络,无需密码
    ap.config(essid='ESP32-RS485-Web', password='', authmode=0)
    ap.ifconfig(('192.168.4.1', '255.255.255.0', '192.168.4.1', '8.8.8.8'))

    while not ap.active():
        pass

    print('AP模式已启动 - 开放网络')
    print('网络名称: ESP32-RS485-Web')
    print('IP地址: 192.168.4.1')
    return ap
# 初始化RS485串口
def setup_rs485(baudrate=4800):
    global uart_instance
    # 如果已有实例,先关闭
    if uart_instance:
        try:
            uart_instance.deinit()
            time.sleep(0.1)
        except:
            pass

    # 创建新的UART实例
    uart_instance = UART(2, baudrate=baudrate, bits=8, parity=None, stop=1, rx=16, tx=17)
    print(f"RS485串口已初始化,波特率: {baudrate}")
    return uart_instance
# 读取RS485数据(增强稳定性)
def read_rs485_data(uart):
    global rs485_data
    try:
        if uart and uart.any():
            data = uart.read()
            if data:
                timestamp = time.ticks_ms()
                hex_data = data.hex()
                ascii_data = ''.join([chr(b) if 32 <= b < 127 else '.' for b in data])

                # 新数据插入到前面
                rs485_data.insert(0, {
                    'timestamp': timestamp,
                    'hex': hex_data,
                    'ascii': ascii_data,
                    'length': len(data)
                })

                # 限制数据列表长度
                if len(rs485_data) > max_data_length:
                    rs485_data = rs485_data[:max_data_length]

                print(f"收到RS485数据: {hex_data} (ASCII: {ascii_data})")
                return True
    except Exception as e:
        print(f"读取RS485数据错误: {e}")
        # 尝试重新初始化串口
        try:
            setup_rs485(current_baudrate)
        except:
            pass

    return False
# 发送数据到RS485
def send_rs485_data(uart, data_str, data_type='hex'):
    try:
        if not uart:
            return False, "串口未初始化"

        if data_type == 'hex':
            data_str = data_str.replace(' ', '').replace('\n', '').replace('\r', '')
            if len(data_str) % 2 != 0:
                return False, "十六进制数据长度必须为偶数"

            try:
                data_bytes = bytes.fromhex(data_str)
            except ValueError:
                return False, "无效的十六进制数据"

        elif data_type == 'ascii':
            data_bytes = data_str.encode('utf-8')
        else:
            return False, "未知的数据类型"

        uart.write(data_bytes)
        print(f"发送数据到RS485: {data_bytes.hex()}")
        return True, "数据发送成功"

    except Exception as e:
        return False, f"发送失败: {str(e)}"
# 生成数据表格HTML
def generate_data_table():
    global rs485_data, display_format

    data_rows = ""
    if not rs485_data:
        data_rows = "<tr><td colspan='4' style='text-align: center;'>暂无数据</td></tr>"
    else:
        for i, data in enumerate(rs485_data[:50]):
            time_str = f"{time.ticks_ms() - data['timestamp']}ms前"
            data_content = data['hex'] if display_format == "hex" else data['ascii']

            data_rows += f"""
            <tr>
                <td>{i+1}</td>
                <td>{time_str}</td>
                <td>{data_content}</td>
                <td>{data['length']}</td>
            </tr>
            """

    return data_rows
# 生成网页HTML
def generate_html(status_message=""):
    global rs485_data, current_baudrate, display_format

    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>ESP32 RS485监控</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
            .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
            h1 {{ color: #333; text-align: center; }}
            .section {{ margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }}
            table {{ width: 100%; border-collapse: collapse; margin: 10px 0; }}
            th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
            th {{ background-color: #f2f2f2; }}
            .form-group {{ margin: 10px 0; }}
            label {{ display: inline-block; width: 150px; }}
            input, select, textarea {{ padding: 5px; margin: 5px 0; }}
            button {{ background-color: #4CAF50; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }}
            button:hover {{ background-color: #45a049; }}
            .btn-danger {{ background-color: #f44336; }}
            .btn-danger:hover {{ background-color: #da190b; }}
            .status {{ padding: 10px; margin: 10px 0; border-radius: 4px; }}
            .success {{ background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }}
            .error {{ background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }}
            #data-table {{ max-height: 400px; overflow-y: auto; display: block; }}
            .new-data {{ background-color: #e8f5e8; transition: background-color 2s; }}
            .display-selector {{ margin-left: 20px; }}
            .connection-status {{ display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }}
            .connected {{ background-color: #4CAF50; }}
            .disconnected {{ background-color: #f44336; }}
        </style>
        <script>
            var currentDisplayFormat = '{display_format}';
            var isConnected = true;

            // 刷新数据表格
            function refreshData() {{
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function() {{
                    if (xhr.readyState === 4) {{
                        if (xhr.status === 200) {{
                            document.getElementById('data-body').innerHTML = xhr.responseText;
                            document.getElementById('data-count').innerText = document.getElementById('data-body').rows.length;

                            // 高亮新数据
                            var rows = document.getElementById('data-body').getElementsByTagName('tr');
                            if (rows.length > 0) {{
                                rows[0].classList.add('new-data');
                                setTimeout(function() {{
                                    if (rows.length > 0) {{
                                        rows[0].classList.remove('new-data');
                                    }}
                                }}, 2000);
                            }}
                            updateConnectionStatus(true);
                        }} else {{
                            updateConnectionStatus(false);
                        }}
                    }}
                }};
                xhr.open('GET', '/data?format=' + currentDisplayFormat, true);
                xhr.timeout = 5000;
                xhr.ontimeout = function() {{
                    updateConnectionStatus(false);
                }};
                xhr.send();
            }}

            // 更新连接状态显示
            function updateConnectionStatus(connected) {{
                isConnected = connected;
                var statusElement = document.getElementById('connection-status');
                var statusText = document.getElementById('connection-status-text');

                if (connected) {{
                    statusElement.className = 'connection-status connected';
                    statusText.innerText = '已连接';
                    statusText.style.color = '#4CAF50';
                }} else {{
                    statusElement.className = 'connection-status disconnected';
                    statusText.innerText = '连接中断';
                    statusText.style.color = '#f44336';
                }}
            }}

            // 切换显示格式
            function switchDisplayFormat() {{
                var formatSelect = document.getElementById('display-format');
                currentDisplayFormat = formatSelect.value;

                var header = document.getElementById('data-content-header');
                header.innerText = currentDisplayFormat === 'hex' ? '数据内容 (十六进制)' : '数据内容 (ASCII)';

                refreshData();
            }}

            // 修改波特率
            function updateBaudrate() {{
                var baudrate = document.getElementById('baudrate').value;
                var xhr = new XMLHttpRequest();
                xhr.open('POST', '/baudrate', true);
                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                xhr.onload = function() {{
                    if (xhr.status === 200) {{
                        var response = JSON.parse(xhr.responseText);
                        if (response.success) {{
                            showStatus('波特率已修改为: ' + baudrate, 'success');
                            document.getElementById('current-baudrate').innerText = baudrate;
                            // 不刷新页面,继续正常数据更新
                            refreshData();
                        }} else {{
                            showStatus('波特率修改失败: ' + response.message, 'error');
                        }}
                    }} else {{
                        showStatus('网络错误,请检查连接', 'error');
                    }}
                }};
                xhr.onerror = function() {{
                    showStatus('网络错误,请检查连接', 'error');
                }};
                xhr.send('baudrate=' + baudrate);
                return false;
            }}

            // 显示状态消息
            function showStatus(message, type) {{
                var statusDiv = document.getElementById('status-message');
                statusDiv.innerHTML = '<div class="status ' + type + '">' + message + '</div>';
                setTimeout(function() {{
                    statusDiv.innerHTML = '';
                }}, 3000);
            }}

            // 设置定时器
            setInterval(refreshData, 1000);
            window.onload = refreshData;
        </script>
    </head>
    <body>
        <div class="container">
            <h1>ESP32 RS485监控界面</h1>
            <div id="status-message"></div>

            <!-- 接收数据 -->
            <div class="section">
                <h3>接收数据 (最近50条,新数据在前) 
                    <select id="display-format" class="display-selector" onchange="switchDisplayFormat()">
                        <option value="ascii" {'selected' if display_format == 'ascii' else ''}>ASCII</option>
                        <option value="hex" {'selected' if display_format == 'hex' else ''}>十六进制</option>
                    </select>
                    <span style="margin-left: 20px;">
                        <span id="connection-status" class="connection-status connected"></span>
                        <span id="connection-status-text" style="color: #4CAF50;">已连接</span>
                    </span>
                </h3>
                <div id="data-table">
                    <table>
                        <thead>
                            <tr>
                                <th>序号</th>
                                <th>时间</th>
                                <th id="data-content-header">数据内容 (ASCII)</th>
                                <th>长度</th>
                            </tr>
                        </thead>
                        <tbody id="data-body">
                            {generate_data_table()}
                        </tbody>
                    </table>
                </div>
            </div>

            <!-- 系统状态 -->
            <div class="section">
                <h3>系统状态</h3>
                <p><strong>IP地址:</strong> 192.168.4.1</p>
                <p><strong>当前波特率:</strong> <span id="current-baudrate">{current_baudrate}</span> bps</p>
                <p><strong>数据条数:</strong> <span id="data-count">{len(rs485_data)}</span></p>
            </div>

            <!-- 串口配置 -->
            <div class="section">
                <h3>串口配置</h3>
                <form onsubmit="return updateBaudrate()">
                    <div class="form-group">
                        <label for="baudrate">波特率:</label>
                        <select id="baudrate" name="baudrate">
                            <option value="4800" {'selected' if current_baudrate == 4800 else ''}>4800</option>
                            <option value="9600" {'selected' if current_baudrate == 9600 else ''}>9600</option>
                            <option value="19200" {'selected' if current_baudrate == 19200 else ''}>19200</option>
                            <option value="38400" {'selected' if current_baudrate == 38400 else ''}>38400</option>
                            <option value="57600" {'selected' if current_baudrate == 57600 else ''}>57600</option>
                            <option value="115200" {'selected' if current_baudrate == 115200 else ''}>115200</option>
                        </select>
                        <button type="submit">应用波特率</button>
                    </div>
                </form>
            </div>

            <!-- 数据控制 -->
            <div class="section">
                <h3>数据控制</h3>
                <button type="button" class="btn-danger" onclick="clearData()">清除所有数据</button>
            </div>

            <!-- 发送数据 -->
            <div class="section">
                <h3>发送数据</h3>
                <form method="post" action="/send" onsubmit="return sendData()">
                    <div class="form-group">
                        <label for="data_type">数据类型:</label>
                        <select id="data_type" name="data_type">
                            <option value="hex">十六进制</option>
                            <option value="ascii">ASCII文本</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="send_data">发送数据:</label><br>
                        <textarea id="send_data" name="send_data" rows="3" cols="50" placeholder="输入要发送的数据..."></textarea>
                    </div>
                    <div class="form-group">
                        <button type="submit">发送数据</button>
                    </div>
                </form>
            </div>
        </div>
    </body>
    </html>
    """
    return html
# 处理HTTP请求
def handle_http_request(client_socket, uart):
    global rs485_data, current_baudrate, display_format

    try:
        request = client_socket.recv(1024).decode('utf-8')
        print("收到HTTP请求")

        request_line = request.split('\r\n')[0]
        method, path, _ = request_line.split(' ')

        response = ""
        content_type = "text/html"
        status_message = ""

        if method == 'GET' and path == '/':
            response = generate_html()

        elif method == 'GET' and path.startswith('/data'):
            if 'format=' in path:
                format_param = path.split('format=')[1].split('&')[0]
                display_format = format_param if format_param in ['hex', 'ascii'] else 'ascii'

            response = generate_data_table()
            content_type = "text/html"

        elif method == 'POST':
            if path == '/baudrate':
                content_length = 0
                for line in request.split('\r\n'):
                    if line.startswith('Content-Length:'):
                        content_length = int(line.split(':')[1].strip())
                        break

                if content_length > 0:
                    post_data = request.split('\r\n\r\n')[-1][:content_length]
                    params = {}
                    for pair in post_data.split('&'):
                        key, value = pair.split('=')
                        params[key] = value

                    new_baudrate = int(params.get('baudrate', '4800'))
                    try:
                        setup_rs485(new_baudrate)
                        current_baudrate = new_baudrate
                        print(f"波特率已修改为: {new_baudrate}")
                        # 返回JSON响应而不是重定向
                        response = json.dumps({"success": True, "message": "波特率修改成功"})
                        content_type = "application/json"
                    except Exception as e:
                        response = json.dumps({"success": False, "message": str(e)})
                        content_type = "application/json"

            elif path == '/send':
                content_length = 0
                for line in request.split('\r\n'):
                    if line.startswith('Content-Length:'):
                        content_length = int(line.split(':')[1].strip())
                        break

                if content_length > 0:
                    post_data = request.split('\r\n\r\n')[-1][:content_length]
                    params = {}
                    for pair in post_data.split('&'):
                        key, value = pair.split('=')
                        params[key] = value

                    data_type = params.get('data_type', 'hex')
                    send_data = params.get('send_data', '')

                    success, message = send_rs485_data(uart, send_data, data_type)
                    if success:
                        status_message = f"<div class='status success'>{message}</div>"
                    else:
                        status_message = f"<div class='status error'>{message}</div>"

            elif path == '/clear':
                rs485_data = []
                print("数据已清除")
                status_message = "<div class='status success'>数据已清除</div>"

            if content_type == "text/html":
                response = generate_html(status_message)

        # 发送HTTP响应
        http_response = f"""HTTP/1.1 200 OK
Content-Type: {content_type}; charset=utf-8
Connection: close
{response}"""

        client_socket.send(http_response.encode('utf-8'))

    except Exception as e:
        print("HTTP处理错误:", e)
    finally:
        client_socket.close()
# 主函数
def main():
    global current_baudrate, uart_instance

    # 设置AP模式(开放网络)
    ap = setup_ap()

    # 初始化RS485
    uart_instance = setup_rs485(current_baudrate)

    # 创建TCP服务器
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('0.0.0.0', 80))
    server_socket.listen(5)
    server_socket.settimeout(0.5)

    print("Web服务器已启动,端口: 80")
    print("打开浏览器访问: http://192.168.4.1")

    last_memory_clean = time.ticks_ms()

    while True:
        try:
            # 检查RS485数据
            if uart_instance:
                read_rs485_data(uart_instance)

            # 检查HTTP连接
            try:
                client_socket, addr = server_socket.accept()
                print("客户端连接来自:", addr)
                handle_http_request(client_socket, uart_instance)
            except socket.timeout:
                pass
            except OSError as e:
                if e.args[0] != 110:  # 忽略超时错误
                    raise

            # 定期清理内存
            if time.ticks_ms() - last_memory_clean > 30000:
                gc.collect()
                last_memory_clean = time.ticks_ms()

        except Exception as e:
            print("主循环错误:", e)
            time.sleep(1)
if __name__ == "__main__":
    main()

5.点击保存。在Tonny右下角点击micorpython(ESP32).COM7。在右键main.py,点击上传到 /。等待程序上传至esp32.至此软件部分结束。

硬件连接

1.esp32的16引脚接MAX485的R0引脚
2.esp32的17引脚接MAX485的D1引脚
3.其中MAX485的RE、DE引脚接GND
4.esp32的3.3V引脚接MAX485的VIN
5.esp32的GND接MAX485的GND。
6.用充电宝给esp32供电。
7.测试用的RS485的A线(正差分线)接MAX485的A端子。
8.测试用的RS485的B线(正负差分线)接MAX485的B端子。

使用教程

1.手机连接名为 ESP32-RS485-Web 的wifi。
2.浏览器输入192.168.4.1
3.网页如下:

界面1

界面2

界面3

评论

  1. 2 月前
    2026-4-03 15:33:09

    Been using five 88.com for a bit, solid experience so far. The platform is easy to use, and they seem to update the games regularly. My opinion? Give it a whirl: five 88.com

  2. 2 月前
    2026-4-03 15:33:25

    Okay, so Playdoit? Played it last night and I was pleasantly surprised. Runs smoothly, even on my older phone. Good way to kill some time. Time to Playdoit playdoit

  3. 2 月前
    2026-4-03 15:33:42

    Alright, listen up! Superjili Asia is where it’s at. Been playin’ here for a bit and things are lookin’ good. Definitely worth checkin’ out if you’re lookin’ for some action. Check it out here: superjili asia

  4. 2 月前
    2026-4-07 2:28:23

    Okay, Royal Win, I’m ready to win big Lets see if this game is really as good as everyone says. Fingers crossed for some good luck! royal win

  5. 2 月前
    2026-4-07 2:28:39

    luk888… Sounds familiar! If you’re after a place to wager and win, this is definitely one to bookmark. Always something exciting happening there! Learn more by luk888

  6. 2 月前
    2026-4-07 2:28:56

    Gonna try my luck with tai game b66. Wish me luck! You can get it from here: tai game b66

发送评论 编辑评论


				
上一篇
下一篇