"""
Micropython 没有 queue 请用 deque
from collections import deque
"""
import select, socket, sys, network
from collections import deque
station = network.WLAN(network.STA_IF)
local_ip = station.ifconfig()[0] # IP地址st
tcp_port = 5020 # port to listen for requests/providing data
# Create a TCP/IP socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(socket.AF_INET, socket.SOCK_STREAM) # 2 1
server.setblocking(False)
# Bind the socket to the port
server_address = (local_ip, tcp_port) # localhost = 127.0.0.1/192.168.1.108/
print(sys.stderr, "starting up on %s port %s" % server_address)
server.bind(server_address) # OSError: [Errno 112] EADDRINUSE 稍等退出port重试即可
# Listen for incoming connections
server.listen(5)
# Sockets from which we expect to read
# 开始只有服务端,后面进来新连接再放入监控列表inputs,此列表里面是空就会报错:监控什么?
inputs = [server]
print("初始监控列表里面只有1个服务端 =", inputs)
# Sockets to which we expect to write
outputs = []
message_queues = {}
while inputs:
# Wait for at least one of the sockets to be ready for processing
print("\nwaiting for the next event")
# select接收任务和返回结果依据这3个列表
readable, writable, exceptional = select.select(inputs, outputs, inputs)
# Handle inputs
for s in readable:
# s是服务端,只负责接收新连接,接收数据是connection socket对象的事情
if s is server:
# A "readable" server socket is ready to accept a connection
connection, client_address = s.accept() # 有新连接请求
print("new connection from", client_address)
connection.setblocking(False) # 必须是非阻塞的才能并发
inputs.append(connection) # 新连接交给select监控
# Give the connection a queue for data we want to send
# message_queues字典的Key=connection,Value=专用队列
message_queues[connection] = deque([],10)
else:
data = s.recv(512)
if data:
# A readable client socket has data
# s.getpeername() => AttributeError: 'socket' object has no attribute 'getpeername'
print(sys.stderr, 'received "%s" from %s' % (data, s))
# 有数据来就必有已准备好的专用队列,查字典put数据,交给select了
message_queues[s].extend(data)
# Add output channel for response
# 给select交代工作了:我有输出请处理
if s not in outputs:
outputs.append(s)
else:
# Interpret empty result as closed connection
# closing ('192.168.1.110', 55648) after reading no data
print("closing", client_address, "after reading no data")
# Stop listening for input on the connection
# 既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
if s in outputs:
outputs.remove(s)
inputs.remove(s) # inputs中也删除掉
s.close() # 把这个连接关闭掉
# Remove message queue
del message_queues[s]
# Handle outputs
for s in writable:
try:
next_msg = message_queues[s].pop()
except : # queue.Empty
# No messages waiting so stop checking for writability.
print("output queue for", s, "is empty")
outputs.remove(s) # 利用异常移除output清单里面的任务
else: # s.getpeername() => AttributeError: 'socket' object has no attribute 'getpeername'
print('sending "%s" to %s' % (next_msg, s))
s.send(str(next_msg).encode('utf-8'))
# Handle "exceptional conditions"
# 异常列表清单是select监控input任务socke的结果
for s in exceptional:
print("handling exceptional condition for", s.getpeername())
# Stop listening for input on the connection
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
# Remove message queue
del message_queues[s]
单片机boot.py初始化wifi连接
# This file is executed on every boot (including wake-boot from deepsleep)
# 20240902 采用machine.idle()后出现2次打印wlan.ifconfig()信息:自动调用一遍、主动调用一遍
#import esp
#esp.osdebug(None)
#import webrepl
#webrepl.start()
import utime, machine
from umqtt.simple import MQTTClient # MQTT库
import network # Wi-Fi功能所在库
# Wi-Fi SSID和Password设置
# wifiSsid = "GTP"
# wifiPassword = "csgl123456"
# wifiSsid = "GROUNDWORK1"
# wifiPassword = "admin@123"
# wifiSsid = "HY25F餐厅1"
# wifiPassword = "13875933099"
wifiSsid = "WIFI"
wifiPassword = "abc12345"
# wifiSsid = "gljkzx"
# wifiPassword = "gljkzx2017"
# wifiSsid = "iot_ws"
# wifiPassword = "121121lc"
# 等待Wi-Fi成功连接到路由器
def wait_for_wifi_connection():
wlan = network.WLAN(network.STA_IF) # 创建WLAN对象,STA模式
if not wlan.isconnected():
wlan.active(True) # 激活界面
wlan.scan() # 扫描接入点
print("start to connect ", wifiSsid)
wlan.connect(wifiSsid, wifiPassword) # 连接到指定的路由器(路由器名称为wifiSsid, 密码为:wifiPassword)
while not wlan.isconnected():
machine.idle() # save power while waiting
# pass
ifconfig = wlan.ifconfig() # 获取接口的IP/netmask/gw/DNS地址
print(' ESP32 IP NETMASK GATEWAY DNS')
print(ifconfig)
# 获取并打印信号强度
rssi = wlan.status('rssi')
print('Signal strength:', rssi)
wait_for_wifi_connection()
从上位机python实例移植到mpy,select模块的多路IO复用机制OK,3个问题要解决,例如:
问题1:deque.extend与append方法的差异。
解决办法:message_queues[s].append(data)
# extend(data)将把data拆分成当个ASCII字符对应的int这不是想要的结果
问题2:如何得到socket客户端连接属性? s.getpeername() => AttributeError: 'socket' object has no attribute 'getpeername',只有accept在建立之初传进来的信息,使用后的socket没这方法了。暂时绕开,估计得等mpy固件升级。怀疑是无法获取socke客户端信息,导致了下面问题3。
问题3:outputs任务列表加入的socket对象,在writable循环里面竟然找不到队列的内容了message_queues[s].popleft()为空并非空,而是s不晓得发生了什么变化,接着在outputs.remove(s)时报错,说s不在队列里面。micropython的socket暂不研究了。
解决办法:把pop发送信息为空时才触发异常处理的outputs.remove(s)提前正常完成。这样可能导致消息队列的内容没有发送完成这就终结了outputs任务吗?
next_msg = message_queues[s].popleft()
s.send(next_msg)
outputs.remove(s)
20250330续
用deepseek搜索select.poll()案例并调试,感觉比select.select()高级简单些,例如3个客户端连ESP32服务端正常:
select_poll_test3.py
import socket, select, machine
server_socket = socket.socket()
try:
server_socket.bind(('0.0.0.0', 8080)) # 0000绑定任意可用IP
except Exception as e:
machine.reset()
server_socket.listen(3) # 允许3个挂起连接
print(server_socket.fileno()) # 54
poller = select.poll() # select.select没有select.poll高级
poller.register(server_socket, select.POLLIN)
clients = {} # {fd: (conn, address)} 文件描述用二元组socket.accept()
while True:
events = poller.poll(1000) # 1秒超时
#print(events) # [] --> [(<socket>, 1)]有事件发生就处理
for s, event in events:
#print(fd) # <socket>
if s == server_socket: # 新连接 .fileno()
conn, addr = server_socket.accept()
print("新客户端:", addr) # ('192.168.2.104', 53064)
conn.setblocking(False)
poller.register(conn, select.POLLIN)
clients[conn] = (conn, addr)
conn.send(b"Connected!\r\n")
else: # 客户端数据
conn, addr = clients[s]
data = conn.recv(512)
if data:
print(f"{addr} 发送:", data)
conn.send(data) # Echo 回传
else: # 断开
print(f"{addr} 断开")
poller.unregister(conn)
conn.close()
del clients[conn]
3月31日继续改进,增加deque消息队列缓存,理解POLLOUT、register、unregister概念
在试错中进步
import socket, select, machine, time
from collections import deque
server_socket = socket.socket()
try:
server_socket.bind(('0.0.0.0', 502)) # 0000绑定任意可用IP
except Exception as e:
machine.reset()
server_socket.listen(3) # 允许3个挂起连接
print(server_socket.fileno()) # 54 fileno不是fd
poller = select.poll() # select.select VS select.poll✔
poller.register(server_socket, select.POLLIN)
clients = {} # {conn : (conn, address)} 客户簿 socket.accept()
messages = {} # {conn : deque} 消息簿 POLLOUT TO CLIENTS
while True:
'''最多等待1秒:期间有事件立即返回,否则1秒后返回空列表[]
它决定了轮询行为的工作方式:周期性任务+I/O监听,合理超时可减少空转功耗
1秒超时,平衡响应速度与系统资源占用'''
events = poller.poll(1000)
# print(events) # [] 无-->有 [(<socket>, 1)] 注意:打印明文我们并无法观察到套接字差别
# POLLIN = 1 | POLLOUT = 4 事件发生后以此判断读写操作
for s, event in events:
if s == server_socket: # 监听()->新连接
conn, addr = server_socket.accept()
print("新客户端:", addr) # ('192.168.2.104', 53064)
conn.setblocking(False) # 必须非阻塞
'''概念重点理解 select.POLLIN|select.POLLOUT 同时监控读写?NO.
有发送需求后才监视POLLOUT事件,用完就取消,否则被这事件淹没了!!!
长期监视POLLIN是正确的坚持,但是POLLOUT是用完就得删除的!'''
poller.register(conn, select.POLLIN)
clients[conn] = (conn, addr) # 访客登记薄(新连接:(IP,PORT))
messages[conn] = deque((),3) # 消息队列缓存(新连接:新消息队列可缓存3个消息)
conn.send(b"Welcome!Connected.\r\n")
else: # 通信()->客户端数据
conn, addr = clients[s]
if event == 1:
data = conn.recv(512)
if data:
print(f"{addr} 发送:", data)
# conn.send(data) # Echo回传/不直接回传使用deque缓存
messages[conn].append(data) # 排队待发
# 读写同时监视,重点"可以发送了就通知我",同时兼顾POLLIN
# 此刻只看POLLOUT不管POLLIN就成单向通信了,瞬间不会出问题
poller.register(conn, select.POLLOUT | select.POLLIN)
else: # 客户已断开,清场三部曲
print(f"{addr} 客户已断开,清场三部曲")
poller.unregister(conn) # 取消注册
conn.close() # 断开连接
del clients[conn] # 删除登记
else:
print(events) # [(<socket>, 4)] [(<socket>, 5)]
conn.send(messages[conn].pop()) # 消息弹出发送
poller.register(conn, select.POLLIN) # 注意不是unregister,而是改为POLLIN即可
使用三旺NP301串口服务器后,对比umodbus2.3.7modbus slaver(300个寄存器差不多到顶了),觉得VSP虚拟串口TCP透传产品配合上位机应用程序采集RS485设备数据更灵活(例如随时添加删除设备/标签/采集时间间隔的差异化,这些靠单片机做网关来实现很难)。还有一种考虑是VSP应该可以实现ESP32的远程调试(UART0)。
试验1:20250426 在电脑上安装一款虚拟串口软件(Ebyte这款免费不需破解可用),建立VSP com10 / TCP Client,用串口调试助手打开VSP com10, TCP Client需要指定连接的TCP Server(IP地址+PORT端口)。ESP32单片机MPY编程,使用select.poll机制多路IO复用,注册的不仅有socket套接字,还有uart串口。所谓透传就是:上位机应用程序打开VSP,通过TCPClient连接ESP32上运行的TCPServer程序,TCPC发给TCPS的内容原封不动从MCU的UART0/1/2发出去,如果是MODBUS轮询设备,就会有反馈的信息(可以加工处理,例如IEEE754浮点数转换),MCU再通过socket转给TCPC;如果是UART0就是MCU的远程调试信息,双向都是透传。如此,MPY单线程select.poll可以有多路socket和uart的IO并行,既可满足远程调试(修改程序)又可用于数据采集和简单加工。
对上位机用户而言,VSP虚拟串口的打开使用,就是透过TCP/IP网络在操作现场的MCU串口,突破了距离的限制。还可以满足定制化需求,将数据加工后再转发。另外MCU多路socket和uart之间的各种组合通信也是灵活指定了。
UART1默认timeout=0,只要有接收bytes,内核poll就产生POLLIN事件,这导致接收的信息不完整且内核poll多次产生事件。
timeout=50ms或其他合理延时(视波特率和bytes信息长短由公式确定),目的是等待缓冲区信息接收完整后,内核poll再产生事件。
设置超时简单,检测结束符号可以参考deepseek答案,不再赘述。
UART(1,baudrate=9600,bits=8,parity=None,stop=1,tx=22,rx=23,txbuf=256,rxbuf=256,timeout=50,timeout_char=0,irq=0)
''' UART1RW.py
20250420 串口透传 VSP-TCPC <--> TCPS-UART-RS485
UART1/2=RS485 P23 P22 9600 8N1
'''
from machine import UART
'''
20250427 keep studying
UART1默认timeout=0,只要有接收bytes,内核poll就产生POLLIN事件,这导致接收的信息不完整且内核poll多次产生事件。
timeout=50ms或其他合理延时(视波特率和bytes信息长短由公式确定),目的是等待缓冲区信息接收完整后,内核poll再产生事件。
设置超时简单,检测结束符号可以参考deepseek答案,不再赘述。
UART(1,baudrate=9600,bits=8,parity=None,stop=1,tx=22,rx=23,txbuf=256,rxbuf=256,timeout=50,timeout_char=0,irq=0)
def send_command(cmd):
print("Sending:", cmd)
uart.write(cmd + '\r\n')
# 等待响应
response = b''
timeout = time.ticks_add(time.ticks_ms(), 1000) # 1秒超时
while time.ticks_ms() < timeout:
if uart.any():
response += uart.read()
if b'\n' in response: # 检测到结束符
break
time.sleep_ms(10)
return response.decode('utf-8').strip()
'''
ums = UART(1, tx=22, rx=23, baudrate=9600, timeout=50)
if __name__ == "__main__":
pass
'''
20250408 学而时习之
select.poll✔并发socket通信必须知识点
注册socket和uart方便二者异步通信
'''
import socket, select, machine, time
from collections import deque
import UART1RW
server_socket = socket.socket()
try: # 0000绑定任意可用IP,就看boot.py如何指定,增加了灵活度
server_socket.bind(('0.0.0.0', 8888))
except Exception as e:
machine.reset()
server_socket.listen(5) # 允许5个挂起连接
print('fileno不是fd,server_socket.fileno() =', server_socket.fileno()) # 54
poller = select.poll() # select.select VS select.poll✔
poller.register(server_socket, select.POLLIN) # S0
poller.register(UART1RW.ums, select.POLLIN) # U1
clients = {} # {conn : (conn, address)} 客户簿 socket.accept()
messages = {} # {conn : deque} 消息簿 POLLOUT TO CLIENTS
msg_u = {} # {uart : deque}
while True:
events = poller.poll(3000)
print(events) # [] 无-->有 [(<socket>, 1)]
# POLLIN = 1 | POLLOUT = 4 事件发生后以此判断读写操作
for fd, event in events:
# S监听()->新连接 S0,1
if fd == server_socket and event == select.POLLIN:
conn, addr = server_socket.accept()
print("新客户端:", addr) # ('192.168.2.104', 53064) type(addr)=tuple
conn.setblocking(False) # 必须非阻塞
'''概念重点理解 select.POLLIN|select.POLLOUT 同时监控读写?NO.
有发送需求后才监视POLLOUT事件,用完就取消,否则被这事件淹没了!!!
长期监视POLLIN是正确的坚持,但是POLLOUT是用完就得删除的!'''
poller.register(conn, select.POLLIN)
clients[conn] = (conn, addr) # 访客登记薄(新连接fd, (IP,PORT))
messages[conn] = deque((),10) # 消息队列缓存(新连接:新消息队列可缓存3个消息)
conn.send(b"Welcome!Connected.") # 字符串转字节码
conn.send(bytes(str(addr), 'utf-8')) # str(tuple) 元组转字符串转字节码
# S通信()->客户端数据 !S0!U1,1
if not fd == server_socket and not fd == UART1RW.ums and event == select.POLLIN:
conn, addr = clients[fd]
data = conn.recv(1024)
msg_u[UART1RW.ums] = deque((),10)
if data:
print(f"FROM {addr} COME IN MSG :", data)
# conn.send(data) # Echo回传/不直接回传使用deque缓存
msg_u[UART1RW.ums].append(data) # KeyError: UART(1,...)
# 读写同时监视,重点"可以发送了就通知我",同时兼顾POLLIN
# 此刻只看POLLOUT不管POLLIN就成单向通信了,瞬间不会出问题
poller.register(UART1RW.ums, select.POLLOUT)
else: # 客户已断开,清场三部曲
print(f"{addr} 客户已断开,清场三部曲")
poller.unregister(conn) # POLL取消注册
conn.close() # 套接字断开连接
del clients[conn] # 字典删除客户登记
# U1->TTL U1,4
if fd == UART1RW.ums and event == select.POLLOUT:
msgx = msg_u[fd].pop()
UART1RW.ums.write(msgx) # UART1发出上位机VSP收到的APP数据
print(f'{msgx} UART1RW.ums.write(msgx)')
poller.register(UART1RW.ums, select.POLLIN) # 注意不是unregister,而是改为POLLIN即可
# TTL->U1 U1,1
if fd == UART1RW.ums and event == select.POLLIN:
print('U1->S1')
time.sleep_ms(10) # 必须延时 内核通知很及时
data = UART1RW.ums.read()
print(data)
for keys in clients.keys():
if not keys == server_socket and not keys == UART1RW.ums:
print('send to S1',clients[keys])
conn.send(data)
实验2:建立3个socket与3个uart对应,uart0用于远程调试,uart1/2用于现场modbus从站设备轮询,可透传也可加工再传。这里选择透传,因为上位机的应用程序比较强(任意加减从站,任意寄存器的任意轮询间隔均可,IEEE754浮点数转换,高低字排序,应有尽有,太强了),下位机不需要做什么数据加工,偶尔遇到不标准的浮点数表示方法的设备,加工除以系数10/100/1000也是简单的事情。下面代码,uart0远程调试还没搞通,uart1/2并行透传可以了,供参考:
'''
tcpservs.py 20250502
通过端口号区分应用 ESP32的uart0/1/2对应不同的socket
select.poll✔并发socket通信
'''
import socket, select, machine, time
from collections import deque
import uarts
uart0_socket = socket.socket()
uart1_socket = socket.socket()
uart2_socket = socket.socket()
sockets = [uart0_socket, uart1_socket, uart2_socket] # 套接字元老列表
uart012 = [uarts.uart1, uarts.uart2] # 串口列表
try: # 0000绑定任意可用IP
uart0_socket.bind(('0.0.0.0', 40000))
uart1_socket.bind(('0.0.0.0', 40001))
uart2_socket.bind(('0.0.0.0', 40002))
except Exception as e:
machine.reset()
# 暂时允许1个挂起连接
uart0_socket.listen(1)
uart1_socket.listen(1)
uart2_socket.listen(1)
poller = select.poll() # select.select VS select.poll✔
poller.register(uart0_socket, select.POLLIN) # 不支持列表操作
poller.register(uart1_socket, select.POLLIN) # 逐个添加也不多
poller.register(uart2_socket, select.POLLIN)
poller.register(uarts.uart1, select.POLLIN) # uart1
poller.register(uarts.uart2, select.POLLIN) # uart2
clients = {} # {conn : (conn, address, uart)} 客户簿 socket.accept()
toclients = {} # 反向字典 uart : conn
messages = {} # {conn : deque} 消息簿 POLLOUT TO CLIENTS
msg_u = {} # {uart : deque}
while True:
events = poller.poll(3000)
# print(events) # [] 无-->有 [(<socket>, 1)]
# POLLIN = 1 | POLLOUT = 4 事件发生后以此判断读写操作
for fd, event in events:
# S监听()->新连接 fd在套接字元老列表里面
if fd in sockets and event == select.POLLIN :
if fd == uart0_socket :
conn, addr = uart0_socket.accept()
clients[conn] = (conn, addr, uarts.uart0) # 访客登记薄(新连接fd, (IP,PORT), uart)
toclients[uarts.uart0] = conn
print('uart0_socket.accept port=40000')
elif fd == uart1_socket :
conn, addr = uart1_socket.accept()
clients[conn] = (conn, addr, uarts.uart1) # 访客登记薄(新连接fd, (IP,PORT), uart)
toclients[uarts.uart1] = conn
print('uart1_socket.accept port=40001')
else :
conn, addr = uart2_socket.accept()
clients[conn] = (conn, addr, uarts.uart2) # 访客登记薄(新连接fd, (IP,PORT), uart)
toclients[uarts.uart2] = conn
print('uart2_socket.accept port=40002')
print("新客户端:", addr) # ('192.168.2.104', 53064) type(addr)=tuple
conn.setblocking(False) # 非阻塞
'''select.POLLIN|select.POLLOUT 同时监控读写?NO
有发送需求后才监视POLLOUT事件,用完就即删,否则被这事件淹没
必须长期监视POLLIN,但POLLOUT用完即删'''
poller.register(conn, select.POLLIN)
messages[conn] = deque((),10) # 消息队列缓存(新连接:新消息队列可缓存3个消息)
conn.send(b"Welcome!Connected.") # 字符串转字节码
conn.send(bytes(str(addr), 'utf-8')) # str(tuple) 元组转字符串转字节码
# S通信()->客户端数据
if not fd in sockets and not fd in uart012 and event == select.POLLIN:
conn, addr, uart = clients[fd] # conn -> uart
data = conn.recv(1024)
# print(f'recieve msg to {uart}')
msg_u[uart] = deque((),10)
if data:
print(f"{addr}:", data)
# conn.send(data) # Echo回传/不直接回传使用deque缓存
msg_u[uart].append(data) # KeyError: UART(1,...)
# 读写同时监视,重点"可以发送了就通知我",同时兼顾POLLIN
# 此刻只看POLLOUT不管POLLIN就成单向通信了,瞬间不会出问题
poller.register(uart, select.POLLOUT)
else: # 客户已断开,清场三部曲
print(f"{addr} 客户已断开,清场三部曲")
poller.unregister(conn) # POLL取消注册
conn.close() # 套接字断开连接
del clients[conn] # 字典删除客户登记
# U.write send
if fd in uart012 and event == select.POLLOUT:
msgx = msg_u[fd].pop()
fd.write(msgx) # UART透传上位机VSP的轮询指令
# print(f'{fd} tx')
poller.register(fd, select.POLLIN) # 注意不是unregister,而是改为POLLIN即可
# U.recv read
if fd in uart012 and event == select.POLLIN:
# time.sleep_ms(10) # 必须延时 内核通知很及时
data = fd.read()
print(f'{str({fd})[1:7].replace('(', '')}: {data}')
toclients[fd].send(data)
由于UART0 默认被 REPL(串口终端)占用,直接使用会导致
OSError: (-259,'ESP_ERR_INVALID_STATE'),因此需要先释放 UART0,再建立 Socket 通信。
这个还没搞定,所以暂时搁置研究:UART0 数据转发到 TCP Socket(远程调试),REPL重定向到TCP Socket更好,又多出一个uart0可用。
'''
uarts.py 20250503
串口透传 VSP-TCPC <--> TCPS-UART-RS485
UART1/2=RS485 P23 P22 9600 8N1
'''
import machine, time, network, os
from machine import Timer, UART
from time import sleep_ms
'''
20250427 keep studying
UART1默认timeout=0,只要有接收bytes,内核poll就产生POLLIN事件,这导致接收的信息不完整且内核poll多次产生事件。
timeout=50ms或其他合理延时(视波特率和bytes信息长短由公式确定),目的是等待缓冲区信息接收完整后,内核poll再产生事件。
设置超时简单,检测结束符号可以参考deepseek答案,不再赘述。
UART(1,baudrate=9600,bits=8,parity=None,stop=1,tx=22,rx=23,txbuf=256,rxbuf=256,timeout=50,timeout_char=0,irq=0)
def send_command(cmd):
print("Sending:", cmd)
uart.write(cmd + '\r\n')
# 等待响应
response = b''
timeout = time.ticks_add(time.ticks_ms(), 1000) # 1秒超时
while time.ticks_ms() < timeout:
if uart.any():
response += uart.read()
if b'\n' in response: # 检测到结束符
break
time.sleep_ms(10)
return response.decode('utf-8').strip()
'''
# # 1. 先关闭 REPL 对 UART0 的占用
# os.dupterm(None) # 禁用 REPL
#
# # 2. 初始化 UART0
# uart0 = UART(0, tx=1, rx=3, baudrate=115200)
# # uart0 = machine.UART(0, baudrate=115200)
# # 3. 使用 UART0 发送/接收数据
# uart0.write("Hello from UART0\n")
# data = uart0.read(10) # 读取数据
# print("Received:", data)
#
# # 4. 恢复 REPL(如果需要重新启用终端调试)
# os.dupterm(machine.UART(0, 115200)) # 重新绑定 REPL
uart1 = UART(1, tx=22, rx=23, baudrate=9600, timeout=20)
uart2 = UART(2, tx=21, rx=19, baudrate=9600, timeout=20)
# os.dupterm(UART(0, baudrate=115200)) # 恢复 REPL
if __name__ == "__main__":
pass
实验3:os.dupterm(TCP Socket 重定向)。REPL输出正常,输入接收后无应答,UART0的ctrl+c等仍然有效。MCU里面运行程序的print是可以重定向的,但是REPL并无法重定向。
import os
import machine
import socket
import network
# 初始化 UART0 和 Socket
s = socket.socket()
s.bind(("0.0.0.0", 40000))
s.listen(1)
def enable_uart_repl():
os.dupterm(None) # 切换回 UART0
print("REPL 已切换到 UART0")
def enable_socket_repl(conn):
os.dupterm(conn) # 切换到 Socket
print("REPL 已切换到 Socket")
# 主循环
conn = None
while True:
if conn is None:
conn, addr = s.accept()
# conn.setblocking(False) # 非阻塞模式
enable_socket_repl(conn)
else:
try:
data = conn.recv(1024) # 保持连接
print(data.decode())
if not data:
conn.close()
enable_uart_repl()
conn = None
except:
conn.close()
enable_uart_repl()
conn = None
非阻塞模式不可取:
REPL 已切换到 Socket
REPL 已切换到 UART0
REPL 已切换到 Socket
REPL 已切换到 UART0
REPL 已切换到 Socket
REPL 已切换到 UART0
REPL 已切换到 Socket
REPL 已切换到 UART0
REPL 已切换到 Socket
REPL 已切换到 UART0
阻塞模式表明socket已经接收到信息,但是REPL无应答或者仅应答回车换行:
thonny选择虚拟串口com25,发送import os、os、import select、1+1等,得到的回应只有回车换行,没有内容。
同时在com6口有usb-ttl串口监听uart0,能收到MCU程序的print和REPL发出的回车换行。也就是说程序运行的输出是可以重定向到TCP Socket或者Uart1,但是REPL的交互式功能并未重定向,也就无法远程调试(主要是下载和修改程序)。暂停研究,这个看来暂时无法解决。
data = conn.recv(1024) # 保持连接
print(data.decode())
折腾良久,隐约看见micropython ESP32不能做什么了,期待日后更新的mpy官方固件改进远程REPL,我们能用上thonny远程调试(修改和下载源代码)。
20250504总结:
main.py
'''
tcpservs.py 20250504
通过端口号区分应用 ESP32的uart0/1/2对应不同的socket
select.poll✔并发socket通信
'''
import socket, select, machine, time
from collections import deque
import uarts
# uart0_socket = socket.socket()
uart1_socket = socket.socket()
uart2_socket = socket.socket()
sock12 = [uart1_socket, uart2_socket] # 套接字元老
uart12 = [uarts.uart1, uarts.uart2] # 串口列表
try: # 0000绑定任意可用IP
# uart0_socket.bind(('0.0.0.0', 40000))
uart1_socket.bind(('0.0.0.0', 40001))
uart2_socket.bind(('0.0.0.0', 40002))
except Exception as e:
machine.reset()
# 暂时允许1个挂起连接
# uart0_socket.listen(1)
uart1_socket.listen(1)
uart2_socket.listen(1)
poller = select.poll() # select.select VS select.poll✔
# poller.register(uart0_socket, select.POLLIN) # 不支持列表操作
poller.register(uart1_socket, select.POLLIN) # 逐个添加也不多
poller.register(uart2_socket, select.POLLIN)
poller.register(uarts.uart1, select.POLLIN) # uart1
poller.register(uarts.uart2, select.POLLIN) # uart2
tcpc2u = {} # {conn : (conn, address, uart)} 客户簿 socket.accept()
uart2c = {} # 反向字典 uart : conn
msg_c = {} # {conn : deque} 消息簿
msg_u = {} # {uart : deque} 消息簿
while True:
events = poller.poll(3000)
# print(events) # [] 无-->有 [(<socket>, 1)]
# POLLIN = 1 | POLLOUT = 4 事件发生后以此判断读写操作
for fd, event in events:
# S监听()->新连接 fd在套接字元老列表里面
if fd in sock12 and event == select.POLLIN :
if fd == uart1_socket :
conn, addr = uart1_socket.accept()
tcpc2u[conn] = (conn, addr, uarts.uart1) # 访客登记薄(新连接fd, (IP,PORT), uart)
uart2c[uarts.uart1] = conn
print('uart1_socket.accept port=40001')
else :
conn, addr = uart2_socket.accept()
tcpc2u[conn] = (conn, addr, uarts.uart2) # 访客登记薄(新连接fd, (IP,PORT), uart)
uart2c[uarts.uart2] = conn
print('uart2_socket.accept port=40002')
print("新客户端:", addr) # ('192.168.2.104', 53064) type(addr)=tuple
conn.setblocking(False) # 非阻塞
'''select.POLLIN|select.POLLOUT 同时监控读写?NO
有发送需求后才监视POLLOUT事件,用完就即删,否则被这事件淹没
必须长期监视POLLIN,但POLLOUT用完即删'''
poller.register(conn, select.POLLIN)
msg_c[conn] = deque((),3) # 消息队列缓存(新连接:新消息队列可缓存3个消息)
conn.send(b"Welcome!Connected.") # 字符串转字节码
conn.send(bytes(str(addr), 'utf-8')) # str(tuple) 元组转字符串转字节码
# S通信()->客户端数据
if not fd in sock12 and not fd in uart12 and event == select.POLLIN:
conn, addr, uart = tcpc2u[fd] # conn -> uart
data = conn.recv(256)
# print(f'recieve msg to {uart}')
msg_u[uart] = deque((),3)
if data:
print(f"master cmd {addr}:", data)
# conn.send(data) # Echo回传/不直接回传使用deque缓存
msg_u[uart].append(data) # KeyError: UART(1,...)
# 读写同时监视,重点"可以发送了就通知我",同时兼顾POLLIN
# 此刻只看POLLOUT不管POLLIN就成单向通信了,瞬间不会出问题
poller.register(uart, select.POLLOUT)
else: # 客户已断开,清场三部曲
print(f"{addr} 客户已断开,清场三部曲")
poller.unregister(conn) # POLL取消注册
conn.close() # 套接字断开连接
del tcpc2u[conn] # 字典删除客户登记
# U.write send
if fd in uart12 and event == select.POLLOUT:
msgx = msg_u[fd].pop()
fd.write(msgx) # UART透传上位机VSP的轮询指令
# print(f'{fd} tx')
poller.register(fd, select.POLLIN) # 注意不是unregister,而是改为POLLIN即可
# U.recv read
if fd in uart12 and event == select.POLLIN:
# time.sleep_ms(10) # 必须延时 内核通知很及时
data = fd.read()
print(f'mcu feedback {str({fd})[1:7].replace('(', '')}: {data}')
uart2c[fd].send(data)