#!/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()