基于485总线的评分系统实验报告
信安2201 ZZzz_TT
同组成员:要做好青年(可以去看看他的主页)
验收分数:95 (验收时被老师高度认可)
(报告中前面的套话部分和实验任务一这里直接略过。实验任务一只要按照学习通上一步步做就行,非常简单,如果一直过不了考虑是不是板子或线不行。本文主要展示双机/多机评分的可视化实现)
2.2 实验任务二
任务名称:A级任务
2.21 实验步骤
在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运行结果分析
多机验收时有六块板子,其中上位机为中间右边那个,有五块板子作为下位机,三个分数已确认(分数分别为40,40,60),一个分数未确认(未按下K1键),一个分数超过100分。(因为板子的灯一直闪所以拍摄的不是很清晰)
代码运行界面:
首先它会列出检测到的串口,选择正确的USB串口并选择配置文件后,程序开始运行,并且能通过进度条直观地看到查询进度。
然后点击查询从机状态,显示如下(按下了K2键的从机):
接下来可以点击查看从机分数查看每个从机的分数:
这与上面我们设置的板子的分数是一致的。
这时只需要按下未确认分数的板子的K1键即可确认分数,再次点击查看从机分数会有如下显示:
接下来点击计算平均分按钮((40+40+60+70+0)/5=42分):
接下来点击导出数据(这里只需要输入目标文件的名字并保存即可,默认为csv格式):
导出的文件如下:
接下来点击复位所有从机:
板子的提示灯就会熄灭,可以重新设置分数:
最后点击停止操作,此时可以重新选择串口和配置文件继续评分,或者直接关闭窗口结束运行:
经过多次测试本代码都能稳定运行,但由于杜邦线通信不稳定加上实验的器材太过古老,运行时如果输出有问题,可以尝试多点几次功能按键,例如平均分算错了可以多点几次计算平均分。