HNU电子测试平台与工具2——基于485总线的评分系统(双机/多机可视化)

基于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格式):

导出的文件如下:

接下来点击复位所有从机:

板子的提示灯就会熄灭,可以重新设置分数:

最后点击停止操作,此时可以重新选择串口和配置文件继续评分,或者直接关闭窗口结束运行:

经过多次测试本代码都能稳定运行,但由于杜邦线通信不稳定加上实验的器材太过古老,运行时如果输出有问题,可以尝试多点几次功能按键,例如平均分算错了可以多点几次计算平均分。

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值