前言:手里恰好有一个空余的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.网页如下:




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
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
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
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
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
Gonna try my luck with tai game b66. Wish me luck! You can get it from here: tai game b66