Files
dps150-python/main.py
2026-02-13 22:52:33 +03:00

534 lines
23 KiB
Python
Executable File
Raw 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.
#!/usr/bin/env python3
"""
DPS-150 Power Supply Management Utility
Полнофункциональная утилита для управления блоком питания DPS-150
"""
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import serial.tools.list_ports
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.animation as animation
from collections import deque
import threading
import time
from dps150 import DPS150
from dps150.packets.cmd import (
VOLTAGE_SET, CURRENT_SET, OUTPUT_ENABLE, OVP, OCP, OPP, OTP, LVP
)
class DPS150GUI:
def __init__(self, root):
self.root = root
self.root.title("DPS-150 Управление блоком питания")
self.root.geometry("1400x900")
self.device = None
self.connected = False
self.data = {}
# Данные для графиков (храним последние 200 точек)
self.max_points = 200
self.time_data = deque(maxlen=self.max_points)
self.voltage_data = deque(maxlen=self.max_points)
self.current_data = deque(maxlen=self.max_points)
self.power_data = deque(maxlen=self.max_points)
self.start_time = time.time()
self.setup_ui()
self.update_serial_ports()
def setup_ui(self):
"""Создание интерфейса"""
# Главный контейнер
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
# Левая панель - управление
left_panel = ttk.Frame(main_frame)
left_panel.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
# Правая панель - графики
right_panel = ttk.Frame(main_frame)
right_panel.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(0, weight=1)
self.setup_connection_panel(left_panel)
self.setup_info_panel(left_panel)
self.setup_control_panel(left_panel)
self.setup_protection_panel(left_panel)
self.setup_log_panel(left_panel)
self.setup_graphs_panel(right_panel)
def setup_connection_panel(self, parent):
"""Панель подключения"""
frame = ttk.LabelFrame(parent, text="Подключение", padding="10")
frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
ttk.Label(frame, text="COM порт:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.port_var = tk.StringVar(value="/dev/ttyACM0")
port_combo = ttk.Combobox(frame, textvariable=self.port_var, width=20, state="readonly")
port_combo.grid(row=0, column=1, padx=5, pady=5)
self.port_combo = port_combo
ttk.Button(frame, text="Обновить", command=self.update_serial_ports).grid(
row=0, column=2, padx=5, pady=5
)
self.connect_btn = ttk.Button(
frame, text="Подключиться", command=self.toggle_connection
)
self.connect_btn.grid(row=1, column=0, columnspan=3, pady=10, sticky=(tk.W, tk.E))
self.status_label = ttk.Label(frame, text="Отключено", foreground="red")
self.status_label.grid(row=2, column=0, columnspan=3, pady=5)
def setup_info_panel(self, parent):
"""Панель информации об устройстве"""
frame = ttk.LabelFrame(parent, text="Информация об устройстве", padding="10")
frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5)
self.info_labels = {}
info_fields = [
("modelName", "Модель:"),
("hardwareVersion", "Версия железа:"),
("firmwareVersion", "Версия прошивки:"),
]
for i, (key, label) in enumerate(info_fields):
ttk.Label(frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
value_label = ttk.Label(frame, text="---", foreground="gray")
value_label.grid(row=i, column=1, sticky=tk.W, padx=10)
self.info_labels[key] = value_label
def setup_control_panel(self, parent):
"""Панель управления"""
frame = ttk.LabelFrame(parent, text="Управление выходом", padding="10")
frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
# Текущие значения
values_frame = ttk.Frame(frame)
values_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
self.value_labels = {}
value_fields = [
("inputVoltage", "Входное напряжение:", "V"),
("outputVoltage", "Выходное напряжение:", "V"),
("outputCurrent", "Выходной ток:", "A"),
("outputPower", "Выходная мощность:", "W"),
("temperature", "Температура:", "°C"),
("outputCapacity", "Ёмкость:", "Ah"),
("outputEnergy", "Энергия:", "Wh"),
]
for i, (key, label, unit) in enumerate(value_fields):
ttk.Label(values_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
value_label = ttk.Label(
values_frame, text="---", font=("Arial", 10, "bold"), foreground="blue"
)
value_label.grid(row=i, column=1, sticky=tk.W, padx=10)
self.value_labels[key] = (value_label, unit)
# Установка значений
set_frame = ttk.Frame(frame)
set_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
ttk.Label(set_frame, text="Установить напряжение (V):").grid(
row=0, column=0, sticky=tk.W, pady=5
)
self.voltage_var = tk.StringVar(value="0.0")
voltage_entry = ttk.Entry(set_frame, textvariable=self.voltage_var, width=10)
voltage_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(
set_frame, text="Установить", command=self.set_voltage
).grid(row=0, column=2, padx=5, pady=5)
ttk.Label(set_frame, text="Установить ток (A):").grid(
row=1, column=0, sticky=tk.W, pady=5
)
self.current_var = tk.StringVar(value="0.0")
current_entry = ttk.Entry(set_frame, textvariable=self.current_var, width=10)
current_entry.grid(row=1, column=1, padx=5, pady=5)
ttk.Button(
set_frame, text="Установить", command=self.set_current
).grid(row=1, column=2, padx=5, pady=5)
# Кнопка включения/выключения
self.output_btn = ttk.Button(
frame, text="Включить выход", command=self.toggle_output, state="disabled"
)
self.output_btn.grid(row=2, column=0, columnspan=2, pady=10, sticky=(tk.W, tk.E))
# Режим работы
mode_frame = ttk.Frame(frame)
mode_frame.grid(row=3, column=0, columnspan=2, pady=5)
ttk.Label(mode_frame, text="Режим:").grid(row=0, column=0, sticky=tk.W)
self.mode_label = ttk.Label(
mode_frame, text="---", font=("Arial", 10, "bold"), foreground="green"
)
self.mode_label.grid(row=0, column=1, padx=10)
# Состояние защиты
ttk.Label(mode_frame, text="Защита:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.protection_label = ttk.Label(
mode_frame, text="---", font=("Arial", 10, "bold"), foreground="orange"
)
self.protection_label.grid(row=1, column=1, padx=10, pady=5)
def setup_protection_panel(self, parent):
"""Панель настроек защиты"""
frame = ttk.LabelFrame(parent, text="Настройки защиты", padding="10")
frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5)
self.protection_vars = {}
protection_fields = [
(OVP, "overVoltageProtection", "OVP (V):"),
(OCP, "overCurrentProtection", "OCP (A):"),
(OPP, "overPowerProtection", "OPP (W):"),
(OTP, "overTemperatureProtection", "OTP (°C):"),
(LVP, "lowVoltageProtection", "LVP (V):"),
]
for i, (cmd, key, label) in enumerate(protection_fields):
ttk.Label(frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
var = tk.StringVar(value="0.0")
entry = ttk.Entry(frame, textvariable=var, width=10)
entry.grid(row=i, column=1, padx=5, pady=2)
def make_setter(cmd_val, var_ref=var):
return lambda: self.set_protection(cmd_val, var_ref.get())
ttk.Button(frame, text="Уст.", command=make_setter(cmd)).grid(
row=i, column=2, padx=5, pady=2
)
self.protection_vars[key] = var
def setup_log_panel(self, parent):
"""Панель логов"""
frame = ttk.LabelFrame(parent, text="Лог", padding="10")
frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
parent.rowconfigure(4, weight=1)
self.log_text = scrolledtext.ScrolledText(
frame, height=8, width=40, wrap=tk.WORD
)
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
def setup_graphs_panel(self, parent):
"""Панель графиков"""
frame = ttk.Frame(parent)
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
parent.columnconfigure(0, weight=1)
parent.rowconfigure(0, weight=1)
# Создаём фигуру с тремя подграфиками
self.fig = Figure(figsize=(10, 8), dpi=100)
# График напряжения
self.ax_voltage = self.fig.add_subplot(311)
self.ax_voltage.set_title("Выходное напряжение", fontsize=10)
self.ax_voltage.set_ylabel("Напряжение (V)", fontsize=9)
self.ax_voltage.grid(True)
self.line_voltage, = self.ax_voltage.plot([], [], 'b-', lw=1.5, label="Напряжение")
self.ax_voltage.legend(fontsize=8)
# График тока
self.ax_current = self.fig.add_subplot(312)
self.ax_current.set_title("Выходной ток", fontsize=10)
self.ax_current.set_ylabel("Ток (A)", fontsize=9)
self.ax_current.grid(True)
self.line_current, = self.ax_current.plot([], [], 'r-', lw=1.5, label="Ток")
self.ax_current.legend(fontsize=8)
# График мощности
self.ax_power = self.fig.add_subplot(313)
self.ax_power.set_title("Выходная мощность", fontsize=10)
self.ax_power.set_xlabel("Время (сек)", fontsize=9)
self.ax_power.set_ylabel("Мощность (W)", fontsize=9)
self.ax_power.grid(True)
self.line_power, = self.ax_power.plot([], [], 'g-', lw=1.5, label="Мощность")
self.ax_power.legend(fontsize=8)
self.fig.tight_layout()
# Встраиваем в tkinter
self.canvas = FigureCanvasTkAgg(self.fig, master=frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Запускаем анимацию
self.ani = animation.FuncAnimation(
self.fig, self.update_graphs, interval=200, blit=False, cache_frame_data=False
)
def log(self, message):
"""Добавить сообщение в лог"""
timestamp = time.strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
self.log_text.see(tk.END)
def update_serial_ports(self):
"""Обновить список доступных COM портов"""
ports = [port.device for port in serial.tools.list_ports.comports()]
if not ports:
ports = ["/dev/ttyACM0", "/dev/ttyUSB0", "COM1", "COM3"]
self.port_combo['values'] = ports
if ports and not self.port_var.get() in ports:
self.port_var.set(ports[0])
def toggle_connection(self):
"""Подключение/отключение от устройства"""
if not self.connected:
self.connect()
else:
self.disconnect()
def connect(self):
"""Подключиться к устройству"""
port = self.port_var.get()
if not port:
messagebox.showerror("Ошибка", "Выберите COM порт")
return
try:
self.log(f"Подключение к {port}...")
self.device = DPS150(port, callback=self.on_data_received)
self.device.start()
time.sleep(0.5)
self.device.get_all()
self.connected = True
self.connect_btn.config(text="Отключиться")
self.status_label.config(text="Подключено", foreground="green")
self.output_btn.config(state="normal")
self.log("Успешно подключено")
# Запускаем периодическое обновление данных
self.schedule_data_update()
except Exception as e:
messagebox.showerror("Ошибка подключения", str(e))
self.log(f"Ошибка подключения: {e}")
if self.device:
try:
self.device.stop()
except:
pass
self.device = None
def disconnect(self):
"""Отключиться от устройства"""
try:
if self.device:
if self.device._running:
self.device.disable()
self.device.stop()
self.connected = False
self.connect_btn.config(text="Подключиться")
self.status_label.config(text="Отключено", foreground="red")
self.output_btn.config(state="disabled")
self.log("Отключено")
self.device = None
# Останавливаем периодическое обновление
if hasattr(self, 'update_job'):
self.root.after_cancel(self.update_job)
except Exception as e:
self.log(f"Ошибка при отключении: {e}")
def on_data_received(self, data):
"""Обработка полученных данных от устройства"""
self.data.update(data)
self.root.after(0, self.update_ui)
def update_ui(self):
"""Обновление интерфейса на основе полученных данных"""
# Информация об устройстве
for key, label in self.info_labels.items():
value = self.data.get(key, "---")
label.config(text=str(value) if value != "---" else "---")
# Текущие значения
for key, (label, unit) in self.value_labels.items():
value = self.data.get(key)
if value is not None:
label.config(text=f"{value:.3f} {unit}")
else:
label.config(text="---")
# Режим работы
mode = self.data.get("mode", "---")
if mode != "---":
self.mode_label.config(text=mode)
# Состояние защиты
protection = self.data.get("protectionState", "---")
if protection and protection != "---":
self.protection_label.config(text=protection, foreground="red")
else:
self.protection_label.config(text="Норма", foreground="green")
# Обновление данных для графиков
current_time = time.time() - self.start_time
voltage = self.data.get("outputVoltage")
current = self.data.get("outputCurrent")
power = self.data.get("outputPower")
if voltage is not None:
self.time_data.append(current_time)
self.voltage_data.append(voltage)
self.current_data.append(current if current is not None else 0)
self.power_data.append(power if power is not None else 0)
# Обновление значений защиты в полях ввода
for key, var in self.protection_vars.items():
value = self.data.get(key)
if value is not None and not var.get():
var.set(f"{value:.2f}")
# Обновление кнопки выхода
self._update_output_button()
def update_graphs(self, frame):
"""Обновление графиков"""
if len(self.time_data) > 0:
time_list = list(self.time_data)
# Обновление графика напряжения
if len(self.voltage_data) > 0:
self.line_voltage.set_data(time_list, list(self.voltage_data))
self.ax_voltage.relim()
self.ax_voltage.autoscale_view()
# Обновление графика тока
if len(self.current_data) > 0:
self.line_current.set_data(time_list, list(self.current_data))
self.ax_current.relim()
self.ax_current.autoscale_view()
# Обновление графика мощности
if len(self.power_data) > 0:
self.line_power.set_data(time_list, list(self.power_data))
self.ax_power.relim()
self.ax_power.autoscale_view()
return [self.line_voltage, self.line_current, self.line_power]
def set_voltage(self):
"""Установить напряжение"""
if not self.connected or not self.device:
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
return
try:
value = float(self.voltage_var.get())
self.device.set_float_value(VOLTAGE_SET, value)
self.log(f"Установлено напряжение: {value} V")
except ValueError:
messagebox.showerror("Ошибка", "Неверное значение напряжения")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось установить напряжение: {e}")
self.log(f"Ошибка установки напряжения: {e}")
def set_current(self):
"""Установить ток"""
if not self.connected or not self.device:
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
return
try:
value = float(self.current_var.get())
self.device.set_float_value(CURRENT_SET, value)
self.log(f"Установлен ток: {value} A")
except ValueError:
messagebox.showerror("Ошибка", "Неверное значение тока")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось установить ток: {e}")
self.log(f"Ошибка установки тока: {e}")
def _update_output_button(self):
"""Обновить текст кнопки выхода на основе текущего состояния"""
if not self.connected:
return
output_closed = self.data.get("outputClosed", False)
if output_closed:
self.output_btn.config(text="Включить выход")
else:
self.output_btn.config(text="Выключить выход")
def toggle_output(self):
"""Включить/выключить выход"""
if not self.connected or not self.device:
return
try:
output_closed = self.data.get("outputClosed", False)
if output_closed:
self.device.enable()
self.log("Выход включен")
else:
self.device.disable()
self.log("Выход выключен")
# Обновление произойдёт при следующем получении данных
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось изменить состояние выхода: {e}")
self.log(f"Ошибка изменения состояния выхода: {e}")
def set_protection(self, cmd, value_str):
"""Установить значение защиты"""
if not self.connected or not self.device:
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
return
try:
value = float(value_str)
self.device.set_float_value(cmd, value)
protection_names = {
OVP: "OVP", OCP: "OCP", OPP: "OPP", OTP: "OTP", LVP: "LVP"
}
name = protection_names.get(cmd, "Защита")
self.log(f"Установлена {name}: {value}")
except ValueError:
messagebox.showerror("Ошибка", "Неверное значение")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось установить защиту: {e}")
self.log(f"Ошибка установки защиты: {e}")
def schedule_data_update(self):
"""Планировать периодическое обновление данных"""
if self.connected and self.device:
try:
self.device.get_all()
except:
pass
if self.connected:
self.update_job = self.root.after(1000, self.schedule_data_update) # Обновление каждую секунду
def on_closing(self):
"""Обработка закрытия окна"""
if self.connected:
self.disconnect()
self.root.destroy()
def main():
root = tk.Tk()
app = DPS150GUI(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
if __name__ == "__main__":
main()