信安2201 要做好青年
同组成员:ZZzz_TT (纯神)
验收分数:95 (验收时要跟老师讲设计的具体思路并演示操作过程,主要操作过程已在下面的文章中体现)
(报告前面的套话部分老师已经预先帮忙填好,简单的前置任务这里不多说,跟着学习通做就行。本文主要展示多机评分的可视化实现)
2.2 实验任务二
任务名称:A级任务
2.2.1 实验步骤
在B级任务基础上,扩充程序功能如:允许最大从机数、轮询次数、错误数据包的处理、统计多人评分的平均分等。
我们进行了以下几点改进:
1. 新增了自动查询所有在线从机的功能,并能返回每一台从机的在线状态和校验信息。
2. 实现了从配置文件config.txt中读取“超时时间”、“允许最大从机数”、“轮询次数”和“是否打印调试信息”的配置。
3. 添加了对错误数据包的处理:对超过100分的分数记为0分;对于未锁定分数的情况,返回错误信息,如果后续确认了分数,再次查询分数时就会读取正确分数。
4. 增加了计算平均分的功能。
5. 引入了调试信息输出的开关“debug”,可以选择性地输出调试信息。
6. 增加了检测并选择串口的功能,防止出现串口冲突的问题。
7. 增加了一键复位所有从机的功能。
8. 增加了将读取的数据导出到csv文件中的功能。
9. 加入了代码的可视化功能,以便更好地理解和检查代码。
其中可视化部分,我们对以上各个功能都设置了对应按钮,按下后即可执行对应功能。此外,我们还设计了进度条的功能,可以显示各项任务的执行过程,让用户能更好的判断任务的执行进度。
2.2.2 程序代码
主程序495.py如下(同目录下还要放配置文件config.txt):
import binascii
import csv
import serial
import serial.tools.list_ports
import time
import os
import tkinter as tk
from tkinter import messagebox, filedialog, ttk
def openreadconfig(file_name):
data = []
with open(file_name, 'r', encoding='utf-8') as file:
file_data = file.readlines()
count = 0
for row in file_data:
if row.strip():
tmp_list = row.split()
tmp_list[-1] = tmp_list[-1].strip()
if count == 0:
data.append(float(tmp_list[1]))
else:
data.append(int(float(tmp_list[1])))
count += 1
return data
def select_port():
plist = list(serial.tools.list_ports.comports())
if not plist:
messagebox.showerror("错误", "没有发现串口")
return None
if len(plist) == 1:
port = plist[0]
messagebox.showinfo("信息", f"只检测到一个串口: {port.device}")
return port.device
else:
port_choices = [f"{port.device} - {port.description}" for port in plist]
return port_choices
def read_times(ser):
while 1:
dic = []
reading = ser.read(5)
if reading != b'':
hex_str = binascii.hexlify(reading).decode('utf-8')
for index in range(0, len(hex_str), 2):
dic.append(int(hex_str[index:index+2], 16))
return dic
def communicate_with_device(ser, data, polling_num, interval=0.2):
for _ in range(polling_num):
ser.write(bytearray(data))
time.sleep(interval)
retdata = read_times(ser)
if retdata:
return retdata
return []
def query_devices(ser, device_upper, polling_num, debug):
devices = list(range(device_upper))
onlineDevices = []
device_info = {}
progress['maximum'] = len(devices)
for idx, device in enumerate(devices):
if stop_flag.get():
break
data = [0x5A, device, 0x08, 0x13]
data.append(sum(data) % 256)
if debug: output_text.insert(tk.END, f"从机设备编号: {device} 发送信息为: {data}\n")
retdata = communicate_with_device(ser, data, polling_num, interval=0.2)
if retdata and len(retdata) >= 2:
if debug: output_text.insert(tk.END, f"返回值:{retdata}\n")
if retdata[1] == device and retdata[-1] == sum(retdata[:-1]) % 256:
onlineDevices.append(device)
device_info[device] = retdata
else:
device_info[device] = "数据校验失败"
else:
device_info[device] = "无返回数据"
progress['value'] = idx + 1
app.update_idletasks()
return onlineDevices, device_info
def read_scores(ser, onlineDevices, polling_num, debug):
global device_scores
total = 0
device_scores = {}
progress['maximum'] = len(onlineDevices)
for idx, device in enumerate(onlineDevices):
if stop_flag.get():
break
data = [0x5A, 0x00, 0x03, device]
data.append(sum(data) % 256)
if debug: print(f"从机设备编号: {device} 发送信息为: {data}")
retdata = communicate_with_device(ser, data, polling_num, interval=0.2)
if retdata and len(retdata) >= 4:
if debug: print(f"返回值:{retdata}")
if retdata[1] == device and retdata[-1] == sum(retdata[:-1]) % 256:
if retdata[3] == 0x6F:
if debug: print(f"读取失败,从机 {device} 分数未确认")
device_scores[device] = "分数未确认"
else:
if retdata[3] > 100:
if debug: print(f"分数错误:从机 {device} 分数超过100, 记为0分")
device_scores[device] = 0
else:
total += retdata[3]
device_scores[device] = retdata[3]
else:
if debug: print(f"从机 {device} 传输结果异常或分数校验失败")
device_scores[device] = "数据校验失败"
else:
if debug: print(f"从机 {device} 无返回数据")
device_scores[device] = "无返回数据"
progress['value'] = idx + 1
app.update_idletasks()
return total, device_scores
def reset_devices(ser):
output_text.insert(tk.END, '-'*50 + '\n')
output_text.insert(tk.END, '从机复位操作:\n')
data = [0x5A, 0x00, 0x01, 0x00]
data.append(sum(data) % 256)
for _ in range(10):
ser.write(bytearray(data))
time.sleep(0.1)
output_text.insert(tk.END, "从机已复位,可以开始下一轮评分。\n")
def open_serial_port(port_device, timeout):
try:
return serial.Serial(port_device, 9600, timeout=timeout)
except serial.SerialException as e:
if 'Permission denied' in str(e):
os.system(f'sudo chmod 666 {port_device}')
return serial.Serial(port_device, 9600, timeout=timeout)
elif 'Input/output error' in str(e):
messagebox.showerror("错误", f"无法打开串口 {port_device}。请确保设备已正确连接并未被其他程序占用。")
else:
raise
def select_port_config():
global ser, config, my_timeout, device_upper, polling_num, debug, onlineDevices, device_info, device_scores
port_device = port_listbox.get(tk.ACTIVE).split()[0]
ser = open_serial_port(port_device, 1)
if not ser:
return
file_name = filedialog.askopenfilename(title="选择配置文件", filetypes=[("Text files", "*.txt")])
if not file_name:
return
config = openreadconfig(file_name)
my_timeout, device_upper, polling_num, debug = config
ser.timeout = my_timeout
progress['value'] = 0
output_text.delete(1.0, tk.END)
output_text.insert(tk.END, "正在查询在线从机信息,请稍候...\n")
app.update_idletasks()
stop_flag.set(False)
onlineDevices, device_info = query_devices(ser, device_upper, polling_num, debug)
if stop_flag.get():
output_text.insert(tk.END, "操作已取消\n")
else:
output_text.insert(tk.END, "在线从机查询完毕\n")
frame1.pack_forget()
frame2.pack(pady=10)
def calculate_average():
output_text.delete(1.0, tk.END)
output_text.insert(tk.END, "正在计算平均分,请稍候...\n")
app.update_idletasks()
stop_flag.set(False)
total, device_scores = read_scores(ser, onlineDevices, polling_num, debug)
if stop_flag.get():
output_text.insert(tk.END, "操作已取消\n")
elif onlineDevices:
valid_scores = [score for score in device_scores.values() if isinstance(score, int)]
average = total / len(valid_scores) if len(valid_scores) > 0 else 0
output_text.insert(tk.END, f"平均分为{average}\n")
else:
output_text.insert(tk.END, "没有在线的从机,无法计算平均分。\n")
def check_status():
output_text.delete(1.0, tk.END)
output_text.insert(tk.END, "正在查询从机状态,请稍候...\n")
app.update_idletasks()
stop_flag.set(False)
output_text.insert(tk.END, "在线从机:\n")
for device in onlineDevices:
retdata = device_info[device]
if isinstance(retdata, list):
output_text.insert(tk.END, f"从机 {device} 在线,校验信息:{retdata}\n")
else:
output_text.insert(tk.END, f"从机 {device} 状态错误: {retdata}\n")
output_text.insert(tk.END, "不在线的从机:\n")
for device in range(device_upper):
if device not in onlineDevices:
output_text.insert(tk.END, f"从机 {device} 不在线\n")
def check_scores():
output_text.delete(1.0, tk.END)
output_text.insert(tk.END, "正在查询从机分数,请稍候...\n")
app.update_idletasks()
stop_flag.set(False)
_, device_scores = read_scores(ser, onlineDevices, polling_num, debug)
if stop_flag.get():
output_text.insert(tk.END, "操作已取消\n")
else:
output_text.insert(tk.END, "从机分数:\n")
for device, score in device_scores.items():
if score == "分数未确认":
output_text.insert(tk.END, f"从机 {device} 分数未确认\n")
elif score == 0:
output_text.insert(tk.END, f"从机 {device} 分数大于100分,记为0分\n")
elif isinstance(score, int):
output_text.insert(tk.END, f"从机 {device} 分数:{score}\n")
else:
output_text.insert(tk.END, f"从机 {device} 状态错误: {score}\n")
def reset_all_devices():
output_text.delete(1.0, tk.END)
output_text.insert(tk.END, "正在复位所有从机,请稍候...\n")
app.update_idletasks()
reset_devices(ser)
output_text.insert(tk.END, "从机已复位,可以开始下一轮评分。\n")
def stop_operation():
stop_flag.set(True)
output_text.insert(tk.END, "正在停止操作...\n")
app.update_idletasks()
# 释放串口资源并回到选择界面
if ser.is_open:
ser.close()
frame2.pack_forget()
frame1.pack(pady=10)
def export_data_to_csv():
if not onlineDevices:
messagebox.showerror("错误", "没有数据可以导出,请先查询设备状态或评分。")
return
file_name = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
if not file_name:
return
with open(file_name, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['Device ID', 'Status', 'Score']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for device, score in device_scores.items():
if isinstance(score, int): # 只导出有评分数据的从机
status = device_info.get(device, "不在线")
writer.writerow({'Device ID': device, 'Status': status, 'Score': score})
messagebox.showinfo("成功", f"数据已成功导出到 {file_name}")
# 创建GUI
app = tk.Tk()
app.title("基于485总线的评分系统")
app.geometry("600x500")
stop_flag = tk.BooleanVar()
onlineDevices = []
device_info = {}
device_scores = {}
frame1 = tk.Frame(app)
frame1.pack(pady=10)
port_label = tk.Label(frame1, text="选择串口:")
port_label.grid(row=0, column=0)
port_choices = select_port()
port_listbox = tk.Listbox(frame1, selectmode=tk.SINGLE, height=len(port_choices))
for choice in port_choices:
port_listbox.insert(tk.END, choice)
port_listbox.grid(row=0, column=1)
select_button = tk.Button(frame1, text="选择配置文件", command=select_port_config)
select_button.grid(row=1, columnspan=2, pady=10)
frame2 = tk.Frame(app)
task_label = tk.Label(frame2, text="选择任务:")
task_label.grid(row=0, column=0)
button_width = 15 # 设置按钮的统一宽度
average_button = tk.Button(frame2, text=" 计算平均分 ", command=calculate_average)
average_button.grid(row=0, column=1, padx=10)
status_button = tk.Button(frame2, text="查看从机状态", command=check_status)
status_button.grid(row=0, column=2, padx=10)
score_button = tk.Button(frame2, text="查看从机分数", command=check_scores)
score_button.grid(row=0, column=3, padx=10)
reset_button = tk.Button(frame2, text="复位所有从机", command=reset_all_devices)
reset_button.grid(row=1, column=2, padx=10)
stop_button = tk.Button(frame2, text=" 停止操作 ", command=stop_operation)
stop_button.grid(row=1, column=3, padx=10)
export_button = tk.Button(frame2, text=" 导出数据 ", command=export_data_to_csv)
export_button.grid(row=1, column=1, padx=10)
progress = ttk.Progressbar(app, orient=tk.HORIZONTAL, length=400, mode='determinate')
progress.pack(pady=10)
output_text = tk.Text(app, height=15, width=70)
output_text.pack(pady=10)
frame1.pack(pady=10)
frame2.pack_forget()
app.mainloop()
配置文件config.txt代码如下:
timeout: 0.04
机器编号范围上限: 10
轮询次数: 20
是否打印调试信息: 1
2.2.3运行结果分析
六块板子如上图所示。其中1块上位板,5块下位板。其中板1,2,3均已确认,分数分别为40、40、60(由于拍摄图片的算法问题可能显示不清晰);板4分数70未确认;板5分数140超过100分。
运行界面:
首先选择正确的USB串口后,开始运行程序,查询从机信息,并有进度条显示查询进度。
然后,点击查询从机状态按钮,显示如下:
可以看到,正确检测到从机1、2、3、4、5在线。
接下来,点击查看从机分数按钮,结果如下:
可以看到,正确显示已确认的板1、2、3的分数40、40、60;板4分数未确认,因此输出提示信息;板5分数超过100分,记为0分。
接下来按下板4的分数确认按钮,板4的分数确认提示灯亮,如下图所示:(其余设置未改变,由于拍摄图片的算法问题可能显示不清晰)
再次点击查看从机分数按钮,结果如下:
此时可以显示板4的正确分数70。
接下来点击计算平均分按钮,结果如下:
正确得到平均分(40+40+60+70+0)/5=42分。
后续,我们对结果进行导出,运行截图如下:
获得导出文件如下:
最后我们进行从机的复位操作,点击复为所有从机按钮:
板子显示如下,可以看到,均成功复位:
最后,我们点击停止操作按钮,本次评分结束。若需要下次评分,选择串口和配置文件后,再次运行该程序即可。
运行结果分析:
我们设置了编号为1、2、3、4、5的从机,其中从机1,2,3均已确认,分数分别为40、40、60;从机4分数70未确认;从机5分数140超过100分。
通过上面的运行结果我们可以看出,程序能够正确的获取已确认从机的分数,同时对分数未确认和分数超过100分这两个错误均实现了错误检查功能。对于超过100分的分数,我们进行的是记为0分的处理;而对于未确认的分数,如果后续确认,再次查询时也可读出正确分数,这些在上面的测试中均有体现。
此外,经过实验,本代码可以在多次测试下稳定运行。
心得体会
本次实验的代码比较复杂,不仅要实现上位机和下位机的通信,还要关注串口通信的延迟参数,这一点非常重要。在实验中,我们进行了多次尝试,期望在正确性和响应时间之间找到平衡。为了达到目的,我们尝试了调整轮询次数或调整延迟参数,最终获得了比较满意的结果。这样做,使得用户与我们的程序进行交互时会感到快速并有效,这是设计评分系统的一大目的。
此外,我们还进行了可视化探索,包括对整个评分系统的交互页面设计,对选择串口、选择配置文件、查看从机状态、查看从机分数、计算平均分、复位所有从机等等不同功能的按钮设计以及任务进度条的设计,这使得用户能更有兴趣地与我们的程序进行交互,并能随时得知任务的具体进展如何,把抽象的程序变得更加具体了。
总的来说,这次试验对我的软硬件设计能力都有一定的提升,对编程知识的实际运用也更有体会了。