cmvr-es/script/USB_UI.py

384 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import serial
import time
import serial.tools.list_ports
from typing import Optional, Dict, List
import logging
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
# 配置日志系统
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 传感器型号与分布力数据长度的映射(单位:字节)
SENSOR_DISTRIBUTED_LENGTH = {
"S1813_elite": 93,
"S2015_elite": 156,
"S1813_core": 153,
"S2716_core": 348,
"S3013_core": 288,
"M2826_omega": 381,
"L3530_omega": 405,
"S1610_elite": 75,
"M2324_core": 204,
"M3025_core": 231,
"L5325_omega": 717,
"M2020_elite": 27
}
class SensorUI:
def __init__(self, root):
self.root = root
self.root.title("传感器数据采集系统")
self.root.geometry("900x700")
self.root.resizable(True, True)
self.ser = None # 串口对象
self.running = False # 数据采集状态
self.init_ui()
def init_ui(self):
# ===== 配置区域 =====
config_frame = ttk.LabelFrame(self.root, text="设备配置")
config_frame.pack(fill=tk.X, padx=10, pady=5)
# COM口选择
ttk.Label(config_frame, text="COM口:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.com_var = tk.StringVar()
self.com_combo = ttk.Combobox(config_frame, textvariable=self.com_var, width=10)
self.com_combo.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(config_frame, text="刷新", command=self.refresh_com_ports).grid(row=0, column=2, padx=5, pady=5)
# 传感器型号选择(联动数据长度)
ttk.Label(config_frame, text="传感器型号:").grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
self.sensor_model = tk.StringVar(value="S1813_elite")
self.sensor_combo = ttk.Combobox(config_frame, textvariable=self.sensor_model, width=15)
self.sensor_combo['values'] = list(SENSOR_DISTRIBUTED_LENGTH.keys())
self.sensor_combo.bind("<<ComboboxSelected>>", self.update_length_by_model) # 选择型号时更新长度
self.sensor_combo.grid(row=0, column=4, padx=5, pady=5)
# 模组号选择(联动设备地址)
ttk.Label(config_frame, text="模组号:").grid(row=0, column=5, padx=5, pady=5, sticky=tk.W)
self.module_id = tk.StringVar(value="00") # 默认模组号00
self.module_combo = ttk.Combobox(config_frame, textvariable=self.module_id, width=5)
self.module_combo['values'] = ["00", "01", "02", "03", "04", "05", "06", "07"]
self.module_combo.bind("<<ComboboxSelected>>", self.update_device_addr) # 选择模组号时更新设备地址
self.module_combo.grid(row=0, column=6, padx=5, pady=5)
# 设备地址显示(只读,由模组号计算得出)
ttk.Label(config_frame, text="设备地址:").grid(row=0, column=7, padx=5, pady=5, sticky=tk.W)
self.device_addr_var = tk.StringVar(value="01") # 模组号00对应设备地址01
ttk.Label(config_frame, textvariable=self.device_addr_var, width=5).grid(row=0, column=8, padx=5, pady=5)
# 数据长度设置(可手动修改)
ttk.Label(config_frame, text="分布力数据长度(字节):").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
self.dist_length = tk.StringVar(value=str(SENSOR_DISTRIBUTED_LENGTH["S1813_elite"]))
self.dist_entry = ttk.Entry(config_frame, textvariable=self.dist_length, width=10)
self.dist_entry.grid(row=1, column=1, padx=5, pady=5)
ttk.Label(config_frame, text="合力数据长度(字节):").grid(row=1, column=2, padx=5, pady=5, sticky=tk.W)
self.result_length = tk.StringVar(value="3")
ttk.Entry(config_frame, textvariable=self.result_length, width=10).grid(row=1, column=3, padx=5, pady=5)
# 控制按钮
btn_frame = ttk.Frame(config_frame)
btn_frame.grid(row=1, column=4, columnspan=5, padx=5, pady=5)
ttk.Button(btn_frame, text="连接设备", command=self.connect_device).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="传感器标定", command=self.calibrate_sensor).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="开始采集", command=self.start_collection).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="停止采集", command=self.stop_collection).pack(side=tk.LEFT, padx=5)
# ===== 日志显示区域 =====
log_frame = ttk.LabelFrame(self.root, text="数据日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, state=tk.DISABLED)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 初始化COM口列表和设备地址
self.refresh_com_ports()
self.update_device_addr()
def refresh_com_ports(self):
"""刷新COM口列表"""
ports = [port.device for port in serial.tools.list_ports.comports()]
self.com_combo['values'] = ports
if ports:
self.com_var.set(ports[0])
def update_device_addr(self, event=None):
"""根据模组号计算设备地址(设备地址 = 模组号 + 1转为十六进制"""
try:
module_dec = int(self.module_id.get(), 16) # 模组号转为十进制
device_dec = module_dec + 1 # 设备地址 = 模组号 + 1
device_hex = f"{device_dec:02X}" # 转为2位十六进制大写
self.device_addr_var.set(device_hex)
self.log(f"模组号{self.module_id.get()} → 设备地址{device_hex}")
except ValueError:
self.log("模组号格式错误,无法计算设备地址")
def update_length_by_model(self, event=None):
"""根据选择的传感器型号自动更新分布力数据长度"""
model = self.sensor_model.get()
if model in SENSOR_DISTRIBUTED_LENGTH:
self.dist_length.set(str(SENSOR_DISTRIBUTED_LENGTH[model]))
self.log(f"自动更新{model}的分布力数据长度为: {SENSOR_DISTRIBUTED_LENGTH[model]}字节")
def log(self, message: str):
"""在UI中显示日志"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def connect_device(self):
"""连接串口设备"""
if self.ser and self.ser.is_open:
self.ser.close()
com_port = self.com_var.get()
if not com_port:
messagebox.showerror("错误", "请选择COM口")
return
try:
self.ser = serial.Serial(
port=com_port,
baudrate=921600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0.1, # 读取超时缩短至0.1秒
write_timeout=0.1, # 写入超时缩短
inter_byte_timeout=0.0005, # 字节间隔超时缩短
xonxoff=False,
rtscts=False
)
if self.ser.is_open:
self.log(f"串口连接成功: {com_port}波特率921600")
logger.info(f"串口连接成功: {com_port}")
except serial.SerialException as e:
self.log(f"串口连接失败: {str(e)}")
logger.error(f"串口连接失败: {str(e)}")
messagebox.showerror("连接失败", str(e))
def get_commands(self) -> Dict[str, str]:
"""生成命令帧"""
model = self.sensor_model.get()
device_addr = self.device_addr_var.get() # 设备地址(十六进制,如"01"
dist_len = int(self.dist_length.get()) # 分布力数据长度(十进制)
# 1. 分布力命令帧:
# 命令帧格式55 AA 09 00 [设备地址] 00 FB 0E 04 00 00 [长度低8位] [长度高8位]
len_low = dist_len & 0xFF # 数据长度低8位
len_high = (dist_len >> 8) & 0xFF # 数据长度高8位
len_hex = f"{len_low:02X}{len_high:02X}" # 小端格式(低位+高位)
distributed_frame = f"55 AA 09 00 {device_addr} 00 FB 0E 04 00 00 {len_hex[0:2]} {len_hex[2:4]}"
# 2. 标定命令帧:
# 格式55 AA 0A 00 [设备地址] 00 79 03 00 00 00 01 00 01
calibration_frame = f"55 AA 0A 00 {device_addr} 00 79 03 00 00 00 01 00 01"
# 3. 合力命令帧:
# 格式55 AA 09 00 [设备地址] 00 FB F0 03 00 00 03 00
resultant_frame = f"55 AA 09 00 {device_addr} 00 FB F0 03 00 00 03 00"
return {
"calibration": calibration_frame,
"resultant_force": resultant_frame,
"distributed_force": distributed_frame
}
def calculate_lrc(self, data: bytes) -> int:
"""计算LRC校验值"""
lrc = 0
for byte in data:
lrc = (lrc + byte) & 0xFF
lrc = ((~lrc) + 1) & 0xFF
return lrc
def send_command(self, command_type: str) -> Optional[bytes]:
"""发送命令并返回响应"""
if not self.ser or not self.ser.is_open:
self.log("未连接设备,请先连接")
return None
commands = self.get_commands()
if command_type not in commands:
self.log(f"未知命令类型: {command_type}")
return None
# 生成带LRC的命令帧
frame = commands[command_type].replace(" ", "") # 移除空格
try:
frame_bytes = bytes.fromhex(frame)
lrc = self.calculate_lrc(frame_bytes)
frame_with_lrc = frame + f"{lrc:02X}" # 追加LRC
except ValueError as e:
self.log(f"生成命令帧失败: {str(e)}")
return None
# 发送命令并打印详情(含设备地址和数据长度)
try:
data_bytes = bytes.fromhex(frame_with_lrc)
self.ser.write(data_bytes)
print(f"发送{command_type}命令(设备地址={self.device_addr_var.get()}: {frame_with_lrc}")
except serial.SerialException as e:
self.log(f"命令发送失败: {str(e)}")
return None
# 读取响应
time.sleep(0.01)
response = b""
start_time = time.time()
while time.time() - start_time < 0.2: # 1.5秒超时
if self.ser.in_waiting > 0:
response += self.ser.read(self.ser.in_waiting) # 一次性读取所有等待的数据,减少循环次数
# 若已读取到预期长度(根据命令类型),提前退出
if command_type == "resultant_force" and len(response) >= 14 + 3:
break
if command_type == "distributed_force" and len(response) >= 14 + int(self.dist_length.get()):
break
# 极短延迟
time.sleep(0.0001)
if response:
print(f"收到响应({len(response)}字节): {response.hex()}")
return response
print("未收到响应")
return None
def calibrate_sensor(self):
"""标定"""
response = self.send_command("calibration")
if response:
# 校验帧头
if response[:2].hex() == "aa55":
self.log(f"{self.sensor_model.get()} 标定成功")
else:
self.log("标定响应格式错误")
else:
self.log("标定失败")
def parse_resultant_force(self, data: bytes) -> Optional[Dict]:
"""解析合力数据"""
if len(data) != int(self.result_length.get()):
print(f"合力数据长度错误(预期{self.result_length.get()}字节,实际{len(data)}字节)")
return None
byte1, byte2, byte3 = data[0], data[1], data[2]
val1 = byte1 if byte1 <= 127 else byte1 - 256
val2 = byte2 if byte2 <= 127 else byte2 - 256
val3 = byte3
# 转换为物理单位
force_x = val1 * 0.1
force_y = val2 * 0.1
force_z = val3 * 0.1
return {
"raw": (byte1, byte2, byte3),
"parsed": (val1, val2, val3),
"force_N": (force_x, force_y, force_z)
}
def parse_distributed_force(self, data: bytes) -> List[Dict]:
"""解析分布力数据"""
parsed = []
total_len = int(self.dist_length.get())
if len(data) < total_len:
print(f"分布力数据长度不足(预期{total_len}字节,实际{len(data)}字节)")
total_len = len(data)
group_count = total_len // 3 # 每3字节一组
for i in range(group_count):
byte1 = data[i*3]
byte2 = data[i*3 + 1]
byte3 = data[i*3 + 2]
val1 = byte1 if byte1 <= 127 else byte1 - 256
val2 = byte2 if byte2 <= 127 else byte2 - 256
val3 = byte3
force_x = val1 * 0.1
force_y = val2 * 0.1
force_z = val3 * 0.1
parsed.append({
"index": i,
"raw": (byte1, byte2, byte3),
"parsed": (val1, val2, val3),
"force_N": (force_x, force_y, force_z)
})
return parsed
def collect_data(self):
"""循环采集数据"""
if not self.running or not self.ser or not self.ser.is_open:
return
# 读取合力数据
result_response = self.send_command("resultant_force")
if result_response and len(result_response) > 14:
result_data = result_response[14:14 + int(self.result_length.get())]
result_parsed = self.parse_resultant_force(result_data)
if result_parsed:
print("\n===== 合力数据 =====")
print(f"合力数据原始字节: 0x{result_parsed['raw'][0]:02X}, 0x{result_parsed['raw'][1]:02X}, 0x{result_parsed['raw'][2]:02X}")
print(f"合力数据解析值: X={result_parsed['parsed'][0]}, Y={result_parsed['parsed'][1]}, Z={result_parsed['parsed'][2]}")
self.log(f"合力数据: X={result_parsed['force_N'][0]:.1f}N, Y={result_parsed['force_N'][1]:.1f}N, Z={result_parsed['force_N'][2]:.1f}N")
self.log(f"")
# 读取分布力数据
dist_response = self.send_command("distributed_force")
if dist_response and len(dist_response) > 14:
dist_data = dist_response[14:14 + int(self.dist_length.get())]
dist_parsed = self.parse_distributed_force(dist_data)
if dist_parsed:
print("\n===== 分布力数据 =====")
print(f"总测点: {len(dist_parsed)}")
for point in dist_parsed:
print(f"测点{point['index']:02d} | 物理值: X={point['force_N'][0]:.1f}N, Y={point['force_N'][1]:.1f}N, Z={point['force_N'][2]:.1f}N")
print(f"------------------------------------------")
# 继续循环采集,10ms间隔
self.root.after(10, self.collect_data)
def start_collection(self):
"""开始数据采集"""
if self.running:
self.log("已在采集数据中")
return
if not self.ser or not self.ser.is_open:
messagebox.showwarning("警告", "请先连接设备")
return
self.running = True
self.log("开始数据采集...")
self.collect_data()
def stop_collection(self):
"""停止数据采集"""
self.running = False
self.log("已停止数据采集")
def on_close(self):
"""关闭窗口时释放资源"""
self.running = False
if self.ser and self.ser.is_open:
self.ser.close()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = SensorUI(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
root.mainloop()