前言
如图所示是市面上常见的OTA压测继电器,通过ch340串口模块完成对继电器的分路控制,这里我编写了一个脚本方便对4路继电器的控制,可以设置开启时间,关闭时间,复位等功能
软件界面
在设备管理器查看串口号后,选中串口号,波特率使用9600,选择要控制的继电器,当选中后状态指示灯变为绿色,这时候再设置开启和关闭时间,启动运动即可完成控制了,为了防止状态错乱,我增加了一个复位按钮,一键把所有继电器状态置为初始状态。
运行界面
启动运行后,文本栏会打印相关显示,指示不同继电器状态
完整代码
代码全部使用python编写,相关依赖的环境较多,我换了电脑后执行经常出问题,就打包成exe运行了。如果有需要exe的可以评论留下邮箱
依赖的库和环境
项目 | 推荐配置 |
---|---|
Python 版本 | Python 3.9 或 3.10(64位) |
操作系统 | Windows 10 / 11 |
GUI 框架 | tkinter (Python 标准库中已内置) |
pyserial>=3.5
库名 | 用途说明 |
---|---|
tkinter | 图形用户界面(标准库) |
serial (pyserial ) | 串口通信控制继电器 |
threading 、time | 多线程控制继电器 + 定时 |
os 、json 、datetime | 有效期校验和配置记录处理 |
代码
import tkinter as tk
from tkinter import ttk, messagebox
import serial
import serial.tools.list_ports
import threading
import time
import os
import json
from datetime import datetime, timedelta
# 有效期检查函数
def check_expiry():
valid_days = 182 # 6个月
file_path = "activated.json"
if os.path.exists(file_path):
with open(file_path, "r") as f:
data = json.load(f)
start_time = datetime.strptime(data["start_time"], "%Y-%m-%d")
else:
start_time = datetime.now()
with open(file_path, "w") as f:
json.dump({"start_time": start_time.strftime("%Y-%m-%d")}, f)
now = datetime.now()
delta = now - start_time
if delta.days > valid_days:
messagebox.showerror("软件已过期", "本程序有效期为6个月,已过期,无法继续使用。")
root.destroy()
# 获取串口列表
def get_serial_ports():
return [port.device for port in serial.tools.list_ports.comports()]
# 串口发送(线程安全)
serial_lock = threading.Lock()
def send_hex_serial_command(port, baudrate, hex_command):
try:
with serial_lock:
with serial.Serial(port, baudrate, timeout=1) as ser:
ser.write(bytes.fromhex(hex_command))
log(f"[发送] {hex_command}")
except Exception as e:
log(f"发送失败: {e}")
# 日志输出
def log(msg):
log_text.config(state='normal')
log_text.insert(tk.END, msg + '\n')
log_text.see(tk.END)
log_text.config(state='disabled')
# 是否处于运行状态
is_running = False
# 控制线程(每路继电器)
def relay_loop(index, port, baudrate, get_on_time, get_off_time):
on_cmds = ["A0 01 01 A2", "A0 02 01 A3", "A0 03 01 A4", "A0 04 01 A5"]
off_cmds = ["A0 01 00 A1", "A0 02 00 A2", "A0 03 00 A3", "A0 04 00 A4"]
while not is_running:
time.sleep(0.1)
try:
while is_running:
if relay_enabled[index].get():
on_time = get_on_time()
off_time = get_off_time()
send_hex_serial_command(port, baudrate, on_cmds[index])
log(f"[继电器{index+1}] 打开,保持 {on_time} 秒")
time.sleep(on_time)
send_hex_serial_command(port, baudrate, off_cmds[index])
log(f"[继电器{index+1}] 关闭,保持 {off_time} 秒")
time.sleep(off_time)
else:
time.sleep(1)
except Exception as e:
log(f"[继电器{index+1}] 错误: {e}")
# 启动按钮功能
def start_control_loop():
global is_running
if is_running:
log("控制已在运行")
return
port = port_var.get()
baudrate = int(baudrate_var.get())
if not port:
messagebox.showerror("错误", "请选择串口")
return
try:
[int(e.get()) for e in on_entries]
[int(e.get()) for e in off_entries]
except ValueError:
messagebox.showerror("错误", "请输入合法整数")
return
is_running = True
start_button.config(style="BigGreen.TButton")
for i in range(4):
t = threading.Thread(
target=relay_loop,
args=(
i,
port,
baudrate,
lambda i=i: int(on_entries[i].get()),
lambda i=i: int(off_entries[i].get())
),
daemon=True
)
t.start()
log("并行继电器控制已启动")
# 启用/禁用开关
def toggle_relay(index):
current = relay_enabled[index].get()
relay_enabled[index].set(not current)
if relay_enabled[index].get():
status_labels[index].config(text="🟢", fg="green")
else:
status_labels[index].config(text="🔴", fg="red")
# 复位按钮功能
def reset_all():
global is_running
is_running = False
for i in range(4):
relay_enabled[i].set(False)
status_labels[i].config(text="🔴", fg="red")
on_entries[i].delete(0, tk.END)
on_entries[i].insert(0, "10")
off_entries[i].delete(0, tk.END)
off_entries[i].insert(0, "10")
start_button.config(style="Big.TButton")
log("已复位:所有继电器禁用,时间重置为10秒")
# 创建主窗口
root = tk.Tk()
root.withdraw() # 启动时先隐藏窗口
check_expiry() # 执行有效期检查
root.deiconify() # 检查通过再显示窗口
root.title("继电器并行控制器(启动前选中,启动后运行)")
# 样式定义
style = ttk.Style()
style.configure("Big.TButton", font=("Arial", 13, "bold"), padding=12)
style.configure("Small.TButton", font=("Arial", 10), padding=5)
style.configure("BigGreen.TButton", font=("Arial", 13, "bold"), padding=12, background="green")
style.map("BigGreen.TButton", background=[("active", "green")])
# 串口设置区域
frame_top = ttk.Frame(root)
frame_top.pack(padx=10, pady=5, anchor="w")
ttk.Label(frame_top, text="串口号:").pack(side='left')
port_var = tk.StringVar()
port_menu = ttk.Combobox(frame_top, textvariable=port_var, values=get_serial_ports(), width=10)
port_menu.pack(side='left', padx=5)
ttk.Label(frame_top, text="波特率:").pack(side='left')
baudrate_var = tk.StringVar(value="9600")
baudrate_menu = ttk.Combobox(frame_top, textvariable=baudrate_var, values=["9600", "115200"], width=10)
baudrate_menu.pack(side='left', padx=5)
# 控制区域表格
frame_time = ttk.Frame(root)
frame_time.pack(padx=10, pady=5, anchor="w")
ttk.Label(frame_time, text="继电器").grid(row=0, column=0, sticky="w")
ttk.Label(frame_time, text="名称").grid(row=0, column=1, sticky="w")
ttk.Label(frame_time, text="状态").grid(row=0, column=2, sticky="w")
ttk.Label(frame_time, text="开时间(秒)").grid(row=0, column=3, sticky="w")
ttk.Label(frame_time, text="关时间(秒)").grid(row=0, column=4, sticky="w")
on_entries = []
off_entries = []
relay_enabled = []
status_labels = []
control_buttons = []
def make_toggle_cmd(i):
return lambda: toggle_relay(i)
for i in range(4):
ttk.Label(frame_time, text=f"{i+1}").grid(row=i+1, column=0, sticky="w")
enable_var = tk.BooleanVar(value=False)
relay_enabled.append(enable_var)
btn = ttk.Button(frame_time, text=f"继电器{i+1}", command=make_toggle_cmd(i), width=10)
btn.grid(row=i+1, column=1, sticky="w")
control_buttons.append(btn)
status = tk.Label(frame_time, text="🔴", fg="red", font=("Arial", 12))
status.grid(row=i+1, column=2, sticky="w")
status_labels.append(status)
on_entry = ttk.Entry(frame_time, width=8)
on_entry.insert(0, "10")
on_entry.grid(row=i+1, column=3, sticky="w")
on_entries.append(on_entry)
off_entry = ttk.Entry(frame_time, width=8)
off_entry.insert(0, "10")
off_entry.grid(row=i+1, column=4, sticky="w")
off_entries.append(off_entry)
start_button = ttk.Button(
frame_time,
text="启动运行",
command=start_control_loop,
style="Big.TButton",
width=14
)
start_button.grid(
row=1,
column=6,
rowspan=4,
padx=(40, 0),
pady=(20, 0),
sticky="ns"
)
# 日志输出
log_text = tk.Text(root, height=12, state='disabled')
log_text.pack(padx=10, pady=5, fill='both', expand=True)
# 复位按钮
frame_reset = ttk.Frame(root)
frame_reset.pack(padx=10, pady=(0, 5), anchor="e")
reset_btn = ttk.Button(frame_reset, text="复位", command=reset_all, style="Small.TButton")
reset_btn.pack()
# 启动主循环
root.mainloop()