前言:手里恰好有一个空余的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线(带有通讯功能)用于调试。
步骤
软件部分
- 电脑下载安装好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.网页如下:



