更新MRtool

This commit is contained in:
RB 2025-05-25 01:11:07 +08:00
parent 511f9f4da8
commit 544b3745d5

View File

@ -16,7 +16,12 @@ import numpy as np
import pandas as pd
import requests
import webbrowser
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import QGroupBox, QGridLayout, QLineEdit, QTextBrowser
from PyQt5.QtCore import QTimer, pyqtSlot
from PyQt5.QtWidgets import QCheckBox
from PyQt5.QtCore import QTimer
def resource_path(relative_path):
"""兼容PyInstaller打包后资源路径"""
@ -735,7 +740,607 @@ class DownloadPage(QWidget):
main_layout.addWidget(footer)
# --------- 功能三:串口助手 ---------
# class SerialAssistant(QWidget):
class SerialAssistant(QWidget):
def __init__(self):
super().__init__()
self.setFont(QFont("微软雅黑", 15))
self.ser = None
self.timer = None
self.recv_buffer = b""
self.plot_data = {}
self.curve_colors = ["#e74c3c", "#2980b9", "#27ae60", "#f1c40f", "#8e44ad", "#16a085"]
self.data_types = ["float", "int16", "uint16", "int8", "uint8"]
self.data_type = "float"
self.data_count = 2
self.sample_idx = 0
# 新增HEX模式复选框
self.hex_send_chk = QCheckBox("HEX发送")
self.hex_recv_chk = QCheckBox("HEX接收")
self.hex_send_chk.setFont(QFont("微软雅黑", 10))
self.hex_recv_chk.setFont(QFont("微软雅黑", 10))
self.hex_send_chk.setChecked(False)
self.hex_recv_chk.setChecked(False)
# 主体布局
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(32, 32, 32, 32)
main_layout.setSpacing(28)
# 左侧面板
left_panel = QVBoxLayout()
left_panel.setSpacing(20)
left_panel.setContentsMargins(0, 0, 0, 0)
# 串口配置区
config_group = QGroupBox("串口配置")
config_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
config_group.setStyleSheet("""
QGroupBox {
border: 2px solid #b5d0ea;
border-radius: 12px;
margin-top: 12px;
background: #f8fbfd;
color: #2471a3;
padding: 8px 0 0 0;
}
QGroupBox:title {
subcontrol-origin: margin;
left: 16px;
top: -8px;
background: transparent;
padding: 0 8px;
}
""")
config_layout = QGridLayout()
config_layout.setSpacing(12)
config_layout.setContentsMargins(16, 16, 16, 16)
config_layout.addWidget(QLabel("串口号:"), 0, 0)
self.port_box = QComboBox()
self.port_box.setMinimumWidth(120)
self.refresh_ports()
config_layout.addWidget(self.port_box, 0, 1)
config_layout.addWidget(QLabel("波特率:"), 1, 0)
self.baud_box = QComboBox()
self.baud_box.addItems(["9600", "115200", "57600", "38400", "19200", "4800"])
self.baud_box.setCurrentText("115200")
config_layout.addWidget(self.baud_box, 1, 1)
self.refresh_btn = QPushButton("刷新串口")
self.refresh_btn.clicked.connect(self.refresh_ports)
self.refresh_btn.setStyleSheet(self._btn_style())
config_layout.addWidget(self.refresh_btn, 2, 0)
self.open_btn = QPushButton("打开串口")
self.open_btn.setCheckable(True)
self.open_btn.clicked.connect(self.toggle_serial)
self.open_btn.setStyleSheet(self._btn_style("#27ae60"))
config_layout.addWidget(self.open_btn, 2, 1)
config_group.setLayout(config_layout)
left_panel.addWidget(config_group)
# 数据协议配置区
proto_group = QGroupBox("数据协议配置")
proto_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
proto_group.setStyleSheet(config_group.styleSheet())
proto_layout = QGridLayout()
proto_layout.setSpacing(12)
proto_layout.setContentsMargins(16, 16, 16, 16)
proto_layout.addWidget(QLabel("数据数量:"), 0, 0)
self.data_count_spin = QSpinBox()
self.data_count_spin.setRange(1, 16)
self.data_count_spin.setValue(self.data_count)
self.data_count_spin.valueChanged.connect(self.apply_proto_config)
proto_layout.addWidget(self.data_count_spin, 0, 1)
proto_layout.addWidget(QLabel("数据类型:"), 1, 0)
self.data_type_box = QComboBox()
self.data_type_box.addItems(self.data_types)
self.data_type_box.setCurrentText(self.data_type)
self.data_type_box.currentTextChanged.connect(self.apply_proto_config)
proto_layout.addWidget(self.data_type_box, 1, 1)
proto_group.setLayout(proto_layout)
left_panel.addWidget(proto_group)
# 发送数据区
send_group = QGroupBox("发送数据")
send_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
send_group.setStyleSheet("""
QGroupBox {
border: 2px solid #b5d0ea;
border-radius: 12px;
margin-top: 12px;
background: #f8fbfd;
color: #2471a3;
padding: 8px 0 0 0;
}
QGroupBox:title {
subcontrol-origin: margin;
left: 16px;
top: -8px;
background: transparent;
padding: 0 8px;
}
""")
send_layout = QVBoxLayout()
send_layout.setSpacing(14)
send_layout.setContentsMargins(12, 12, 12, 12)
# 输入框(更大更高字体)
self.send_edit = QLineEdit()
self.send_edit.setFont(QFont("Consolas", 22))
self.send_edit.setPlaceholderText("输入要发送的数据...")
self.send_edit.setMinimumHeight(54)
self.send_edit.setMinimumWidth(360)
self.send_edit.setStyleSheet("""
QLineEdit {
background: #f8fbfd;
border-radius: 10px;
border: 1.5px solid #d6eaf8;
font-size: 22px;
padding: 12px 18px;
}
""")
send_layout.addWidget(self.send_edit)
# HEX复选框行字体更小
hex_chk_row = QHBoxLayout()
self.hex_send_chk.setFont(QFont("微软雅黑", 10))
self.hex_recv_chk.setFont(QFont("微软雅黑", 10))
for chk in [self.hex_send_chk, self.hex_recv_chk]:
chk.setStyleSheet("""
QCheckBox {
color: #2471a3;
spacing: 12px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
}
QCheckBox::indicator:checked {
background-color: #2980b9;
border: 1.5px solid #2980b9;
}
QCheckBox::indicator:unchecked {
background-color: #fff;
border: 1.5px solid #b5d0ea;
}
""")
self.hex_send_chk.setChecked(False)
self.hex_recv_chk.setChecked(False)
hex_chk_row.addWidget(self.hex_send_chk)
hex_chk_row.addWidget(self.hex_recv_chk)
hex_chk_row.addStretch(1)
send_layout.addLayout(hex_chk_row)
# 发送按钮
send_btn_row = QHBoxLayout()
self.send_btn = QPushButton("发送")
self.send_btn.clicked.connect(self.send_data)
self.send_btn.setFont(QFont("微软雅黑", 16, QFont.Bold))
self.send_btn.setStyleSheet(self._btn_style("#2980b9"))
send_btn_row.addWidget(self.send_btn)
send_layout.addLayout(send_btn_row)
send_group.setLayout(send_layout)
left_panel.addWidget(send_group, stretch=1) # 让发送区弹性填充
# 清空按钮
self.clear_btn = QPushButton("清空接收和曲线")
self.clear_btn.clicked.connect(self.clear_all)
self.clear_btn.setStyleSheet(self._btn_style("#e74c3c"))
left_panel.addWidget(self.clear_btn)
# 弹性空隙(动态扩展填满)
left_panel.addStretch(1)
# 使用说明始终在最下方
usage_group = QGroupBox("使用说明")
usage_group.setFont(QFont("微软雅黑", 13, QFont.Bold))
usage_group.setStyleSheet("""
QGroupBox {
border: 2px solid #b5d0ea;
border-radius: 10px;
margin-top: 10px;
background: #f8fbfd;
color: #2471a3;
padding: 6px 0 0 0;
}
QGroupBox:title {
subcontrol-origin: margin;
left: 10px;
top: -8px;
background: transparent;
padding: 0 6px;
}
""")
usage_layout = QVBoxLayout()
usage_label = QLabel(
"1. 选择串口号和波特率,点击“打开串口”。\n"
"2. 在“数据协议配置”中选择数据数量和数据类型。\n"
"3. 下位机发送格式:\n"
" 0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n"
" 校验和为包头到最后一个数据字节的累加和的低8位。\n"
"4. 每包数据自动绘制曲线X轴为采样点或时间Y轴为各通道数据。\n"
"5. 支持float/int16/uint16/int8/uint8类型最多16通道。\n"
"6. 可点击“测试绘图”按钮模拟数据包接收效果。"
)
usage_label.setWordWrap(True)
usage_label.setFont(QFont("微软雅黑", 9))
usage_layout.addWidget(usage_label)
usage_group.setLayout(usage_layout)
left_panel.addWidget(usage_group)
main_layout.addLayout(left_panel, 0)
# 右侧面板
right_panel = QVBoxLayout()
right_panel.setSpacing(20)
# 接收区
recv_group = QGroupBox("串口接收区")
recv_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
recv_group.setStyleSheet(config_group.styleSheet())
recv_layout = QVBoxLayout()
self.recv_box = QTextEdit()
self.recv_box.setFont(QFont("Consolas", 13))
self.recv_box.setReadOnly(True)
self.recv_box.setMinimumHeight(120)
self.recv_box.setStyleSheet("""
QTextEdit {
background: #f8fbfd;
border-radius: 10px;
border: 1px solid #d6eaf8;
font-size: 15px;
color: #2c3e50;
padding: 8px;
}
""")
recv_layout.addWidget(self.recv_box)
recv_group.setLayout(recv_layout)
right_panel.addWidget(recv_group)
# 曲线绘图区
plot_frame = QFrame()
plot_frame.setStyleSheet("""
QFrame {
background: #fff;
border-radius: 16px;
border: 1px solid #d6eaf8;
}
""")
plot_shadow = QGraphicsDropShadowEffect(self)
plot_shadow.setBlurRadius(18)
plot_shadow.setOffset(0, 4)
plot_shadow.setColor(Qt.gray)
plot_frame.setGraphicsEffect(plot_shadow)
plot_layout2 = QVBoxLayout(plot_frame)
plot_layout2.setContentsMargins(10, 10, 10, 10)
self.figure = Figure(figsize=(7, 4))
self.canvas = FigureCanvas(self.figure)
plot_layout2.addWidget(self.canvas)
right_panel.addWidget(plot_frame, 2)
main_layout.addLayout(right_panel, 1)
# 定时器接收
self.timer = QTimer(self)
self.timer.timeout.connect(self.read_serial)
# 新增:正弦波测试定时器
self.sine_timer = QTimer(self)
self.sine_timer.timeout.connect(self.send_sine_data)
self.sine_phase = 0
# 默认配置
self.apply_proto_config()
def simulate_data(self):
"""模拟一包数据并自动解析绘图"""
import struct
import random
# 构造协议包
head = 0x55
count = self.data_count
dtype = self.data_type
# 随机生成数据
if dtype == "float":
vals = [random.uniform(-10, 10) for _ in range(count)]
data_bytes = struct.pack(f"<{count}f", *vals)
elif dtype == "int16":
vals = [random.randint(-30000, 30000) for _ in range(count)]
data_bytes = struct.pack(f"<{count}h", *vals)
elif dtype == "uint16":
vals = [random.randint(0, 65535) for _ in range(count)]
data_bytes = struct.pack(f"<{count}H", *vals)
elif dtype == "int8":
vals = [random.randint(-128, 127) for _ in range(count)]
data_bytes = struct.pack(f"<{count}b", *vals)
elif dtype == "uint8":
vals = [random.randint(0, 255) for _ in range(count)]
data_bytes = struct.pack(f"<{count}B", *vals)
else:
vals = [0] * count
data_bytes = b"\x00" * (count * self._type_size())
# 拼包
pkt = bytes([head, count]) + data_bytes
checksum = sum(pkt) & 0xFF
pkt += bytes([checksum])
# 加入接收缓冲区并解析
self.recv_buffer += pkt
self.parse_and_plot_bin()
def _btn_style(self, color="#2980b9"):
return f"""
QPushButton {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #eaf6fb, stop:1 #d6eaf8);
color: {color};
border-radius: 14px;
font-size: 16px;
font-weight: 600;
padding: 8px 0;
border: 1.5px solid #d6eaf8;
letter-spacing: 1px;
}}
QPushButton:hover {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #f8fffe, stop:1 #cfe7fa);
color: #1a6fae;
border: 2px solid #b5d0ea;
}}
QPushButton:pressed {{
background: #e3f0fa;
color: {color};
border: 2px solid #a4cbe3;
}}
"""
def apply_proto_config(self):
self.data_count = self.data_count_spin.value()
self.data_type = self.data_type_box.currentText()
self.plot_data = {i: [[], []] for i in range(self.data_count)}
self.sample_idx = 0
self.update_plot()
def refresh_ports(self):
self.port_box.clear()
ports = serial.tools.list_ports.comports()
for port in ports:
self.port_box.addItem(port.device)
if self.port_box.count() == 0:
self.port_box.addItem("无可用串口")
def toggle_serial(self):
if self.open_btn.isChecked():
port = self.port_box.currentText()
baud = int(self.baud_box.currentText())
try:
self.ser = serial.Serial(port, baud, timeout=0.1)
self.open_btn.setText("关闭串口")
self.recv_box.append(f"已打开串口 {port} @ {baud}bps")
self.timer.start(50)
except Exception as e:
self.recv_box.append(f"打开串口失败: {e}")
self.open_btn.setChecked(False)
else:
if self.ser and self.ser.is_open:
self.ser.close()
self.open_btn.setText("打开串口")
self.recv_box.append("串口已关闭")
self.timer.stop()
def send_data(self):
if self.ser and self.ser.is_open:
data = self.send_edit.text()
try:
if self.hex_send_chk.isChecked():
# HEX模式发送
data_bytes = bytes.fromhex(data.replace(' ', ''))
self.ser.write(data_bytes)
self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}")
else:
self.ser.write(data.encode('utf-8'))
self.recv_box.append(f"发送: {data}")
except Exception as e:
self.recv_box.append(f"发送失败: {e}")
else:
self.recv_box.append("串口未打开,无法发送。")
def send_multi_data(self):
if self.ser and self.ser.is_open:
text = self.send_edit.text()
lines = text.split(";")
for line in lines:
if line.strip():
try:
if self.hex_send_chk.isChecked():
data_bytes = bytes.fromhex(line.strip().replace(' ', ''))
self.ser.write(data_bytes)
self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}")
else:
self.ser.write(line.strip().encode('utf-8'))
self.recv_box.append(f"发送: {line.strip()}")
except Exception as e:
self.recv_box.append(f"发送失败: {e}")
else:
self.recv_box.append("串口未打开,无法发送。")
def read_serial(self):
if self.ser and self.ser.is_open:
try:
data = self.ser.read_all()
if data:
if self.hex_recv_chk.isChecked():
self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}")
else:
try:
self.recv_box.append(f"接收: {data.decode('utf-8', errors='replace')}")
except Exception:
self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}")
self.recv_buffer += data
self.parse_and_plot_bin()
except Exception as e:
self.recv_box.append(f"接收失败: {e}")
def toggle_sine_test(self):
if self.test_btn.isChecked():
self.test_btn.setText("停止测试")
self.sine_phase = 0
self.sine_timer.start(80) # 80ms周期
else:
self.test_btn.setText("测试正弦波(持续)")
self.sine_timer.stop()
def send_sine_data(self):
import struct, math
head = 0x55
count = self.data_count
dtype = self.data_type
t = self.sine_phase
vals = []
for i in range(count):
# 多通道不同相位
val = math.sin(t / 10.0 + i * math.pi / 4) * 10
if dtype == "float":
vals.append(float(val))
elif dtype == "int16":
vals.append(int(val * 1000))
elif dtype == "uint16":
vals.append(int(val * 1000 + 20000))
elif dtype == "int8":
vals.append(int(val * 10))
elif dtype == "uint8":
vals.append(int(val * 10 + 100))
# 打包
if dtype == "float":
data_bytes = struct.pack(f"<{count}f", *vals)
elif dtype == "int16":
data_bytes = struct.pack(f"<{count}h", *vals)
elif dtype == "uint16":
data_bytes = struct.pack(f"<{count}H", *vals)
elif dtype == "int8":
data_bytes = struct.pack(f"<{count}b", *vals)
elif dtype == "uint8":
data_bytes = struct.pack(f"<{count}B", *vals)
else:
data_bytes = b"\x00" * (count * self._type_size())
pkt = bytes([head, count]) + data_bytes
checksum = sum(pkt) & 0xFF
pkt += bytes([checksum])
# 直接走接收流程模拟
self.recv_buffer += pkt
self.parse_and_plot_bin()
# 在接收区实时显示理论值
self.recv_box.append(f"理论: {['%.3f'%v for v in vals]}")
self.sine_phase += 1
def parse_and_plot_bin(self):
# 协议0x55 + 数据数量(1B) + 数据 + 校验(1B)
min_len = 1 + 1 + self.data_count * self._type_size() + 1
while len(self.recv_buffer) >= min_len:
idx = self.recv_buffer.find(b'\x55')
if idx == -1:
self.recv_buffer = b""
break
if idx > 0:
self.recv_buffer = self.recv_buffer[idx:]
if len(self.recv_buffer) < min_len:
break
# 检查数量
count = self.recv_buffer[1]
if count != self.data_count:
self.recv_buffer = self.recv_buffer[2:]
continue
data_bytes = self.recv_buffer[2:2+count*self._type_size()]
checksum = self.recv_buffer[2+count*self._type_size()]
calc_sum = (sum(self.recv_buffer[:2+count*self._type_size()])) & 0xFF
if checksum != calc_sum:
self.recv_box.append("校验和错误,丢弃包")
self.recv_buffer = self.recv_buffer[1:]
continue
# 解析数据
values = self._unpack_data(data_bytes, count)
self.recv_box.append(f"接收: {values}")
for i, v in enumerate(values):
self.plot_data[i][0].append(self.sample_idx)
self.plot_data[i][1].append(v)
if len(self.plot_data[i][0]) > 200:
self.plot_data[i][0].pop(0)
self.plot_data[i][1].pop(0)
self.sample_idx += 1
self.recv_buffer = self.recv_buffer[min_len:]
self.update_plot()
def _type_size(self):
if self.data_type == "float":
return 4
elif self.data_type in ("int16", "uint16"):
return 2
elif self.data_type in ("int8", "uint8"):
return 1
return 4
def _unpack_data(self, data_bytes, count):
import struct
fmt = {
"float": f"<{count}f",
"int16": f"<{count}h",
"uint16": f"<{count}H",
"int8": f"<{count}b",
"uint8": f"<{count}B"
}[self.data_type]
try:
return struct.unpack(fmt, data_bytes)
except Exception:
return [0] * count
def update_plot(self):
self.figure.clear()
ax = self.figure.add_subplot(111)
ax.set_xlabel("采样点", fontsize=14)
ax.set_ylabel("数据值", fontsize=14)
has_curve = False
for idx in range(self.data_count):
color = self.curve_colors[idx % len(self.curve_colors)]
x_list, y_list = self.plot_data.get(idx, ([], []))
if x_list and y_list:
ax.plot(x_list, y_list, label=f"CH{idx+1}", color=color, linewidth=2)
has_curve = True
if has_curve:
ax.legend()
ax.grid(True, linestyle="--", alpha=0.5)
self.canvas.draw()
def clear_all(self):
self.recv_box.clear()
self.plot_data = {i: [[], []] for i in range(self.data_count)}
self.sample_idx = 0
self.update_plot()
# --------- 功能四MRobot架构生成 ---------
class GenerateMRobotCode(QWidget):
def __init__(self):
super().__init__()
self.setFont(QFont("微软雅黑", 15))
self.setStyleSheet("""
QWidget {
background: #f8fbfd;
border-radius: 16px;
padding: 20px;
}
""")
self.init_ui()
def init_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(20)
# 功能说明
desc_label = QLabel("MRobot架构生成工具帮助您快速生成MRobot项目代码。")
desc_label.setFont(QFont("微软雅黑", 14))
desc_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(desc_label)
# 其他UI元素...
# --------- 主工具箱UI ---------
class ToolboxUI(QWidget):
@ -794,7 +1399,7 @@ class ToolboxUI(QWidget):
left_layout.addWidget(logo_label)
# 按钮区
self.button_names = ["主页", "曲线拟合", "功能三", "软件指南"]
self.button_names = ["主页", "曲线拟合", "Mini串口助手", "MR架构配置","软件指南"]
self.buttons = []
for idx, name in enumerate(self.button_names):
btn = QPushButton(name)
@ -865,8 +1470,9 @@ class ToolboxUI(QWidget):
self.page_widgets = {
0: HomePage(), # 主页
1: PolyFitApp(), # 多项式拟合
2: self.placeholder_page("功能三开发中..."),
3: DownloadPage(), # 下载页面
2: SerialAssistant(), # 串口助手
3: GenerateMRobotCode(), # MRobot架构生成
4: DownloadPage(), # 下载页面
}
for i in range(len(self.button_names)):
self.stack.addWidget(self.page_widgets[i])