streamlit串口工具开发尝试
需求
编写一个串口工具,可按所需的格式生成测试指令,指令最后是校验位。
开发
开始用streamlit写发现串口对象只能在发送的时候创建、打开和关闭,一个函数里执行完成所有操作,觉得不好用,无法配置串口号之类的,于是就尝试把串口和界面分开,单独一个py文件处理串口逻辑一个py文件运行界面,该思路完全可行,只是中间需要用socket等方法进行数据通信,感觉把问题复杂化了,于是最终还是改成了strealit中开多线程的方式,界面与串口线程之间采用队列进行数据交互,最终功能也实现了,但是打开串口的时候老是卡一会,而且在命令行没法用Ctrl+C结束进程,反正感觉没有之前用pyqt写的好用,接收代码就没有继续写了,以下代码仅供参考。
python
# -*- coding: utf-8 -*-
import time
import queue
import threading
import streamlit as st
import serial
st.set_page_config(
page_title="Ex-stream-ly Cool App",
page_icon="🧊",
layout="wide",
initial_sidebar_state="expanded",
menu_items={
'Get Help': 'https://www.extremelycoolapp.com/help',
'Report a bug': "https://www.extremelycoolapp.com/bug",
'About': "# This is a header. This is an *extremely* cool app!"
}
)
# 界面线程发送给子线程的数据队列
if 'maintosub_queue' not in st.session_state:
st.session_state.maintosub_queue = queue.Queue()
# 子线程发送给主线程的数据队列
if 'subtomain_queue' not in st.session_state:
st.session_state.subtomain_queue = queue.Queue()
# 子线程停止事件
if "stop_event" not in st.session_state:
st.session_state.stop_event = threading.Event()
# 子线程运行状态
if "thread_running" not in st.session_state:
st.session_state.thread_running = False
# 界面最新数据
if "last_input_data" not in st.session_state:
st.session_state.last_input_data = 5
# 收到新数据后触发rerun
if "need_rerun" not in st.session_state:
st.session_state.need_rerun = False
serial.PARITY_NONE
# 子线程任务:实时接收界面数据并处理
def background_task(stop_event, maintosub_queue, subtomain_queue):
config_value = maintosub_queue.get()
ser=serial.Serial()
ser.port = config_value[0] # 串口设备路径
ser.baudrate = config_value[1] # 波特率
ser.bytesize = serial.EIGHTBITS # 数据位长度为8位
ser.parity = config_value[2] # 无奇偶校验
ser.stopbits = serial.STOPBITS_ONE # 1位停止位
ser.timeout = 1 # 读取超时时间为1秒
try:
ser.open()
except serial.SerialException:
print("串口不存在或无法打开,请检查串口名称和权限")
except PermissionError:
print("没有权限访问串口,请检查用户权限")
except Exception as e:
print(f"打开串口时发生未知错误:{e}")
if ser.is_open:
while not stop_event.is_set():
time.sleep(0.1)
# 检查是否有新数据发送到子线程
if not maintosub_queue.empty():
# 取出最新数据
current_value = maintosub_queue.get()
ser.write(current_value)# 写入数据
print(current_value)
count = ser.inWaiting()
if count > 0:
Read_buffer=ser.read_all() # 我们需要读取的是40个寄存器数据,即40个字节
subtomain_queue.put(str(Read_buffer))
ser.close()
#subtomain_queue.put({'thread_running': False})
# 界面部分
st.title("主线程与子线程传递数据示例")
# 刷新页面时重置数据
st.session_state.need_rerun = True
r24c1,r24c2,r24c3 = st.columns(3)
with r24c1:
uart_comNumber = st.selectbox("comNumber",("COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9"))
with r24c2:
uart_band = st.selectbox("波特率",("115200","38400","921600"))
with r24c3:
uart_verification = st.radio(label = "校验位",
options = ("N","O","E"),
index = 1, # 初始化选项
format_func = lambda x : f"{x}", #这是格式化函数,注意我把原始选项ABC修改了。一般地,跟字典或dataframe结合也很好用。
key = "radio_demo",
help = "我是帮助文字", # 看到组件右上角的问号了没?上去悬停一下。
on_change = None, args = None, kwargs = None)
r25c1,r25c2,r25c3 = st.columns(3)
with r25c1:
if st.button("打开串口", disabled=st.session_state.thread_running):
st.session_state.thread_running = True
st.session_state.stop_event.clear()
st.session_state.maintosub_queue.queue.clear()
st.session_state.maintosub_queue.put((uart_comNumber,uart_band,uart_verification))
st.session_state.subtomain_queue.queue.clear()
st.session_state.last_input_data = 5
if "result" in st.session_state:
del st.session_state.result
thread = threading.Thread(
target=background_task,
args=(st.session_state.stop_event,
st.session_state.maintosub_queue,
st.session_state.subtomain_queue)
)
thread.start()
with r25c2:
if st.button("关闭串口", disabled=not st.session_state.thread_running):
st.session_state.stop_event.set()
with r25c3:
# 显示线程状态
if st.session_state.thread_running :
st.image("green.ico")
else:
st.image("red.ico")
input_data = st.number_input("输入数值", value=5, min_value=5, max_value=100, step=1, key='input_data')
xmqmode = st.radio(label = "工作模式",
options = ("白天连续","白天频闪","夜间连续","夜间频闪"),
index = 1, # 初始化选项
format_func = lambda x : f"{x}", #这是格式化函数,注意我把原始选项ABC修改了。一般地,跟字典或dataframe结合也很好用。
key = "radio_demo1",
help = "我是帮助文字", # 看到组件右上角的问号了没?上去悬停一下。
on_change = None, args = None, kwargs = None)
match xmqmode:
case "白天连续": xmqmode_by=0
case "白天频闪": xmqmode_by=1
case "夜间连续": xmqmode_by=2
case "夜间频闪": xmqmode_by=3
checksum = (0x55 + 0xaa + xmqmode_by + st.session_state.last_input_data + 0x11) & 0xff
sendcmd = bytes([0x55, 0xaa, xmqmode_by, st.session_state.last_input_data, 0x11, checksum])
if st.session_state.thread_running and input_data != st.session_state.last_input_data:
# 将新数据推送到队列(每次变化时触发)
st.session_state.maintosub_queue.put(sendcmd)
st.session_state.last_input_data = input_data
def button_clicked2():
st.session_state.maintosub_queue.put(bytes([0x55, 0xaa, 0x00, 0x05, 0x00, 0x04]))
print(bytes([0x55, 0xaa, 0x00, 0x05, 0x00, 0x04]))
st.button("关闭出光",on_click=button_clicked2)
# 等待子线程处理完数据
while st.session_state.thread_running:
# 队列中有数据则处理完数据后重新刷新页面
if not st.session_state.subtomain_queue.empty():
# 处理子线程发送的最新数据
rev_data = st.session_state.subtomain_queue.get()
st.write(rev_data)
print(rev_data)
time.sleep(0.1)
# 刷新页面
if st.session_state.need_rerun:
st.rerun()
多进程通信问题
用ai生成的多进程通信代码,反正都没通上,不清楚原因,用的是:
python
# common_queue.py - 公共队列模块,所有独立进程共享此Queue
from multiprocessing import Manager
# 创建全局Queue实例,所有导入该模块的进程均使用此实例
manager = Manager()
comm_queue = manager.Queue(maxsize=0) # maxsize指定队列最大容量,0为无限
python
# process1.py - 独立进程1(无亲缘,生产者)
from common_queue import comm_queue
import time
import random
if __name__ == "__main__":
print("进程1启动,开始向队列写入数据...")
count = 0
while True:
count += 1
data = {
"type": "send",
"content": f"测试数据{count}",
"timestamp": time.time()
}
comm_queue.put(data) # 入队,向公共队列写数据
print(f"进程1写入:{data}")
time.sleep(random.uniform(0.5, 1.5)) # 随机延迟,模拟实际业务
python
# process2.py - 独立进程2(无亲缘,消费者)
from common_queue import comm_queue
import time
if __name__ == "__main__":
print("进程2启动,开始从队列读取数据...")
while True:
if not comm_queue.empty():
data = comm_queue.get() # 出队,从公共队列读数据
print(f"进程2读取:{data}")
time.sleep(0.2) # 轮询间隔,降低CPU占用
其他内容
过程中用AI写了一个socket的服务器和客户端程序,测试没问题,也贴出来,万一后面能用上,代码没写完,但是对于socket发送/接收的数据流,可以采用json进行数据格式化,主要就2个函数:
- json.dumps 将 Python 对象编码成 JSON 字符串
- json.loads 将已编码的 JSON 字符串解码为 Python 对象
客户端程序:
python
import socket
import threading
import time
class TCPClient:
def __init__(self, host='127.0.0.1', port=8888):
self.host = host
self.port = port
self.client_socket = None
self.running = False
self.receive_thread = None
def connect(self):
"""连接到TCP服务器"""
try:
# 创建socket对象
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到服务器
self.client_socket.connect((self.host, self.port))
print(f"✅ 已连接到服务器 {self.host}:{self.port}")
# 启动接收消息的线程
self.running = True
self.receive_thread = threading.Thread(target=self.receive_messages)
self.receive_thread.daemon = True
self.receive_thread.start()
except ConnectionRefusedError:
print(f"❌ 无法连接到服务器 {self.host}:{self.port}")
except Exception as e:
print(f"❌ 连接失败: {e}")
finally:
self.disconnect()
def send_messages(self,message):
"""发送消息到服务器"""
try:
if self.running:
if message:
self.client_socket.send(message.encode('utf-8'))
except Exception as e:
print(f"❌ 发送消息时出错: {e}")
def receive_messages(self):
"""接收服务器消息"""
try:
while self.running:
# 接收服务器响应
data = self.client_socket.recv(1024)
if not data:
print("📭 服务器断开连接")
self.running = False
break
# 打印服务器消息
message = data.decode('utf-8')
print(f"📥 服务器回复: {message}", end='')
except ConnectionResetError:
print("📭 连接被服务器重置")
self.running = False
except Exception as e:
if self.running: # 只在运行时报告错误
print(f"❌ 接收消息时出错: {e}")
def disconnect(self):
"""断开连接"""
self.running = False
if self.client_socket:
self.client_socket.close()
print("🔌 已断开与服务器的连接")
if __name__ == "__main__":
client = TCPClient('127.0.0.1', 8888)
client.connect()
服务器程序:
python
import socket
import threading
import time
class TCPServer:
def __init__(self, handle_client=None, host='127.0.0.1', port=8888):
self.host = host
self.port = port
self.server_socket = None
self.running = False
self.handle_client = handle_client
def start(self):
"""启动TCP服务器"""
try:
# 创建socket对象
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置地址重用
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址和端口
self.server_socket.bind((self.host, self.port))
# 开始监听,最多允许5个连接排队
self.server_socket.listen(5)
print(f"✅ TCP服务器启动在 {self.host}:{self.port}")
print("等待客户端连接...")
self.running = True
while self.running:
try:
# 接受客户端连接
client_socket, client_address = self.server_socket.accept()
print(f"🔗 新客户端连接: {client_address}")
# 为每个客户端创建新线程处理
client_thread = threading.Thread(
target=self.handle_client,
args=(client_socket, client_address)
)
client_thread.daemon = True
client_thread.start()
except KeyboardInterrupt:
print("\n🛑 服务器正在关闭...")
break
except Exception as e:
print(f"❌ 接受连接时出错: {e}")
continue
except Exception as e:
print(f"❌ 服务器启动失败: {e}")
finally:
self.stop()
def stop(self):
"""停止服务器"""
self.running = False
if self.server_socket:
self.server_socket.close()
print("🛑 TCP服务器已停止")
def handle_client(client_socket, client_address):
"""处理客户端连接"""
try:
# 发送欢迎消息
# welcome_msg = f"欢迎连接到TCP服务器 {host}:{port}!\n输入 'exit' 断开连接\n"
# client_socket.send(welcome_msg.encode('utf-8'))
while True:
# 接收客户端数据
data = client_socket.recv(1024)
if not data:
print(f"📭 客户端 {client_address} 断开连接")
break
# 解码数据
message = data.decode('utf-8').strip()
print(f"📥 来自 {client_address} 的消息: {message}")
# 处理退出命令
if message.lower() == 'exit':
response = "再见!连接已关闭\n"
client_socket.send(response.encode('utf-8'))
break
# 处理特殊命令
if message.lower() == 'time':
response = f"当前时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
elif message.lower() == 'help':
response = "可用命令:\n time - 获取服务器时间\n exit - 断开连接\n help - 显示帮助\n"
else:
# 回声服务:将消息原样返回
response = f"服务器回复: {message}\n"
# 发送响应
client_socket.send(response.encode('utf-8'))
except ConnectionResetError:
print(f"⚠️ 客户端 {client_address} 强制断开连接")
except Exception as e:
print(f"❌ 处理客户端 {client_address} 时出错: {e}")
finally:
# 关闭客户端socket
client_socket.close()
if __name__ == "__main__":
server = TCPServer(handle_client,'127.0.0.1', 8888)
server.start()