commit 24e419b9555716fcd956fb899fb7666019971513 Author: Aleksey Date: Fri Feb 13 22:52:33 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56c55c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +*/__pycache__/ \ No newline at end of file diff --git a/dps150/__init__.py b/dps150/__init__.py new file mode 100644 index 0000000..5d37d32 --- /dev/null +++ b/dps150/__init__.py @@ -0,0 +1,382 @@ +import serial +import struct +import threading +import time +from typing import Callable, Optional, Union, List +from construct import Container +from dps150.packets.cmd import * +from dps150.packets.base import ( + packet, + float_response, + float3_response, + byte_response, + all_data, +) + + +class DPS150: + def __init__(self, serial_device: str, callback: Optional[Callable] = None) -> None: + """ + Initialize DPS150 power supply controller. + + Args: + serial_device: Serial port path (e.g., '/dev/ttyACM0' or 'COM3') + callback: Optional callback function to receive data updates + """ + self.serial_device = serial_device + self.callback = callback if callback else lambda x: None + self._device: Optional[serial.Serial] = None + self._reader_thread: Optional[threading.Thread] = None + self._running = False + self._buffer = bytearray() + + async def _sleep(self, n: float): + """Sleep helper (async-like using time.sleep)""" + time.sleep(n / 1000.0) + + def start(self) -> None: + """Start communication with the device.""" + print(f'start {self.serial_device}') + self._device = serial.Serial( + port=self.serial_device, + baudrate=115200, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=1, + write_timeout=1 + ) + + if not self._device.is_open: + raise Exception(f'Can\'t open serial port! ({self.serial_device})') + + self._running = True + self._start_reader() + self._init_command() + + def stop(self) -> None: + """Stop communication with the device.""" + print('stop') + if self._device and self._device.is_open: + self.send_command(HEADER_OUT, CMD_SESSION, 0, 0) + self._running = False + if self._reader_thread: + self._reader_thread.join(timeout=2) + self._device.close() + + def _start_reader(self) -> None: + """Start background thread for reading data.""" + print('reading...') + self._reader_thread = threading.Thread(target=self._read_loop, daemon=True) + self._reader_thread.start() + + def _read_loop(self) -> None: + """Main reading loop running in background thread.""" + buffer = bytearray() + while self._running and self._device and self._device.is_open: + try: + if self._device.in_waiting > 0: + data = self._device.read(self._device.in_waiting) + buffer.extend(data) + + # Parse packets using construct + i = 0 + while i < len(buffer) - 6: + if buffer[i] == HEADER_IN and buffer[i + 1] == CMD_GET: + # Try to parse packet + try: + # Check if we have enough data + if i + 4 >= len(buffer): + break + + length = buffer[i + 3] + if i + 4 + length + 1 > len(buffer): + break # Not enough data yet + + # Parse packet + packet_data = bytes(buffer[i:i + 4 + length + 1]) + parsed = packet.parse(packet_data) + + # Validate checksum + if not parsed.checksum_valid: + # Checksum error, skip + i += 1 + continue + + # Remove processed packet from buffer + buffer = buffer[i + len(packet_data):] + i = 0 + + # Process parsed packet + self._parse_packet(parsed) + except Exception as e: + # Parse error, skip byte + i += 1 + else: + i += 1 + + # Keep remaining buffer + if i < len(buffer): + buffer = buffer[i:] + else: + buffer = bytearray() + + time.sleep(0.01) # Small delay to prevent CPU spinning + except Exception as error: + print(f'Read error: {error}') + if not self._running: + break + time.sleep(0.1) + + def _init_command(self) -> None: + """Initialize device with session and baud rate.""" + # Start session + self.send_command(HEADER_OUT, CMD_SESSION, 0, 1) + time.sleep(0.1) + + # Set baud rate (115200 = index 4 in [9600, 19200, 38400, 57600, 115200]) + self.send_command(HEADER_OUT, CMD_BAUD, 0, 5) + time.sleep(0.1) + + # Get device info + self.send_command(HEADER_OUT, CMD_GET, MODEL_NAME, 0) + time.sleep(0.1) + self.send_command(HEADER_OUT, CMD_GET, HARDWARE_VERSION, 0) + time.sleep(0.1) + self.send_command(HEADER_OUT, CMD_GET, FIRMWARE_VERSION, 0) + time.sleep(0.1) + self.get_all() + + def send_command(self, c1: int, c2: int, c3: int, c5: Union[int, List[int], bytes]) -> None: + """ + Send command to device. + + Args: + c1: Header (HEADER_IN=240 or HEADER_OUT=241) + c2: Command (CMD_GET=161, CMD_SET=177, etc.) + c3: Type/parameter + c5: Data (int, list of ints, or bytes) + """ + if isinstance(c5, int): + c5 = bytes([c5]) + elif isinstance(c5, list): + c5 = bytes(c5) + elif not isinstance(c5, bytes): + c5 = bytes(c5) + + c4 = len(c5) + c6 = (c3 + c4) % 0x100 + for val in c5: + c6 = (c6 + val) % 0x100 + + # Build packet using construct + packet_container = Container( + header=c1, + cmd=c2, + type=c3, + length=c4, + data=c5, + checksum=c6, + ) + + command = packet.build(packet_container) + self._send_command_raw(command) + + def send_command_float(self, c1: int, c2: int, c3: int, value: float) -> None: + """Send command with float value.""" + float_bytes = struct.pack(' None: + """Send raw command bytes to device.""" + if self._device and self._device.is_open: + self._device.write(command) + time.sleep(0.05) # 50ms delay as in JS version + + def _parse_packet(self, parsed: Container) -> None: + """Parse incoming data packet using construct structures.""" + if parsed.length == 0: + return + + try: + c3 = parsed.type + c5 = parsed.data + + if c3 == 192: # Input voltage + data = float_response.parse(c5) + self.callback({'inputVoltage': data.value}) + + elif c3 == 195: # Output voltage, current, power + data = float3_response.parse(c5) + self.callback({ + 'outputVoltage': data.value1, + 'outputCurrent': data.value2, + 'outputPower': data.value3, + }) + + elif c3 == 196: # Temperature + data = float_response.parse(c5) + self.callback({'temperature': data.value}) + + elif c3 == 217: # Output capacity + data = float_response.parse(c5) + self.callback({'outputCapacity': data.value}) + + elif c3 == 218: # Output energy + data = float_response.parse(c5) + self.callback({'outputEnergy': data.value}) + + elif c3 == 219: # Output closed/enabled + data = byte_response.parse(c5) + self.callback({'outputClosed': data.value == 1}) + + elif c3 == 220: # Protection state + data = byte_response.parse(c5) + state_idx = data.value + if state_idx < len(PROTECTION_STATES): + self.callback({'protectionState': PROTECTION_STATES[state_idx]}) + + elif c3 == 221: # CC=0 or CV=1 + data = byte_response.parse(c5) + self.callback({'mode': 'CC' if data.value == 0 else 'CV'}) + + elif c3 == 222: # Model name + # String response needs length from parent context + try: + model_name = c5.decode('ascii', errors='ignore').rstrip('\x00') + self.callback({'modelName': model_name}) + except: + pass + + elif c3 == 223: # Hardware version + try: + hw_version = c5.decode('ascii', errors='ignore').rstrip('\x00') + self.callback({'hardwareVersion': hw_version}) + except: + pass + + elif c3 == 224: # Firmware version + try: + fw_version = c5.decode('ascii', errors='ignore').rstrip('\x00') + self.callback({'firmwareVersion': fw_version}) + except: + pass + + elif c3 == 225: # Unknown + data = byte_response.parse(c5) + print(f'Unknown data type 225: {data.value}') + + elif c3 == 226: # Upper limit voltage + data = float_response.parse(c5) + self.callback({'upperLimitVoltage': data.value}) + + elif c3 == 227: # Upper limit current + data = float_response.parse(c5) + self.callback({'upperLimitCurrent': data.value}) + + elif c3 == 255: # All data + if len(c5) >= 139: # Full packet size + data = all_data.parse(c5) + + # Debug unknown data + if len(c5) > 139: + print(f'Packet length: {len(c5)}, unknown: protectionStateRaw={data.protectionStateRaw}, ' + f'unknown1={data.unknown1}, unknownVoltage={data.unknownVoltage}, ' + f'unknownCurrent={data.unknownCurrent}') + + protection_state = PROTECTION_STATES[data.protectionStateRaw] if data.protectionStateRaw < len(PROTECTION_STATES) else "" + + self.callback({ + 'inputVoltage': data.inputVoltage, + 'setVoltage': data.setVoltage, + 'setCurrent': data.setCurrent, + 'outputVoltage': data.outputVoltage, + 'outputCurrent': data.outputCurrent, + 'outputPower': data.outputPower, + 'temperature': data.temperature, + + 'group1setVoltage': data.group1setVoltage, + 'group1setCurrent': data.group1setCurrent, + 'group2setVoltage': data.group2setVoltage, + 'group2setCurrent': data.group2setCurrent, + 'group3setVoltage': data.group3setVoltage, + 'group3setCurrent': data.group3setCurrent, + 'group4setVoltage': data.group4setVoltage, + 'group4setCurrent': data.group4setCurrent, + 'group5setVoltage': data.group5setVoltage, + 'group5setCurrent': data.group5setCurrent, + 'group6setVoltage': data.group6setVoltage, + 'group6setCurrent': data.group6setCurrent, + + 'overVoltageProtection': data.overVoltageProtection, + 'overCurrentProtection': data.overCurrentProtection, + 'overPowerProtection': data.overPowerProtection, + 'overTemperatureProtection': data.overTemperatureProtection, + 'lowVoltageProtection': data.lowVoltageProtection, + + 'brightness': data.brightness, + 'volume': data.volume, + 'meteringClosed': data.meteringClosed, + + 'outputCapacity': data.outputCapacity, + 'outputEnergy': data.outputEnergy, + + 'outputClosed': data.outputClosed, + 'protectionState': protection_state, + 'mode': 'CC' if data.modeRaw == 0 else 'CV', + + 'upperLimitVoltage': data.upperLimitVoltage, + 'upperLimitCurrent': data.upperLimitCurrent, + }) + except Exception as e: + print(f'Parse error: {e}') + import traceback + traceback.print_exc() + + def get_all(self) -> None: + """Request all device data.""" + self.send_command(HEADER_OUT, CMD_GET, ALL, 0) + + def set_float_value(self, type_val: int, value: float) -> None: + """Set float parameter value.""" + self.send_command_float(HEADER_OUT, CMD_SET, type_val, value) + + def set_byte_value(self, type_val: int, value: int) -> None: + """Set byte parameter value.""" + self.send_command(HEADER_OUT, CMD_SET, type_val, value) + + def enable(self) -> None: + """Enable output.""" + self.set_byte_value(OUTPUT_ENABLE, 1) + + def disable(self) -> None: + """Disable output.""" + self.set_byte_value(OUTPUT_ENABLE, 0) + + def start_metering(self) -> None: + """Start metering (accumulate capacity/energy).""" + self.set_byte_value(METERING_ENABLE, 1) + + def stop_metering(self) -> None: + """Stop metering.""" + self.set_byte_value(METERING_ENABLE, 0) + + # Legacy methods for compatibility + def connect(self) -> None: + """Alias for start().""" + self.start() + + def disconnect(self) -> None: + """Alias for stop().""" + self.stop() + + def read(self) -> bytes: + """Legacy read method (not used, data comes via callback).""" + if self._device and self._device.in_waiting > 0: + return self._device.read_all() + return b'' + + def write(self, data: bytes) -> None: + """Legacy write method.""" + if self._device and self._device.is_open: + self._device.write(data) diff --git a/dps150/packets/__pycache__/base.cpython-313.pyc b/dps150/packets/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000..0d61540 Binary files /dev/null and b/dps150/packets/__pycache__/base.cpython-313.pyc differ diff --git a/dps150/packets/__pycache__/cmd.cpython-313.pyc b/dps150/packets/__pycache__/cmd.cpython-313.pyc new file mode 100644 index 0000000..6c5d2bb Binary files /dev/null and b/dps150/packets/__pycache__/cmd.cpython-313.pyc differ diff --git a/dps150/packets/base.py b/dps150/packets/base.py new file mode 100644 index 0000000..decc95d --- /dev/null +++ b/dps150/packets/base.py @@ -0,0 +1,86 @@ +from construct import ( + Byte, + Bytes, + Float32l, + Struct, + Computed, + this, + GreedyBytes, +) + +# Base packet structure +# Format: [header, cmd, type, length, data..., checksum] +packet = Struct( + "header" / Byte, + "cmd" / Byte, + "type" / Byte, + "length" / Byte, + "data" / Bytes(this.length), + "checksum" / Byte, + "checksum_valid" / Computed( + lambda ctx: (ctx.type + ctx.length + sum(ctx.data)) % 0x100 == ctx.checksum + ), +) + +# Data structures for different response types +float_response = Struct( + "value" / Float32l, +) + +float3_response = Struct( + "value1" / Float32l, + "value2" / Float32l, + "value3" / Float32l, +) + +byte_response = Struct( + "value" / Byte, +) + +# All data structure (type 255) +all_data = Struct( + "inputVoltage" / Float32l, # 0-4 + "setVoltage" / Float32l, # 4-8 + "setCurrent" / Float32l, # 8-12 + "outputVoltage" / Float32l, # 12-16 + "outputCurrent" / Float32l, # 16-20 + "outputPower" / Float32l, # 20-24 + "temperature" / Float32l, # 24-28 + "group1setVoltage" / Float32l, # 28-32 + "group1setCurrent" / Float32l, # 32-36 + "group2setVoltage" / Float32l, # 36-40 + "group2setCurrent" / Float32l, # 40-44 + "group3setVoltage" / Float32l, # 44-48 + "group3setCurrent" / Float32l, # 48-52 + "group4setVoltage" / Float32l, # 52-56 + "group4setCurrent" / Float32l, # 56-60 + "group5setVoltage" / Float32l, # 60-64 + "group5setCurrent" / Float32l, # 64-68 + "group6setVoltage" / Float32l, # 68-72 + "group6setCurrent" / Float32l, # 72-76 + "overVoltageProtection" / Float32l, # 76-80 + "overCurrentProtection" / Float32l, # 80-84 + "overPowerProtection" / Float32l, # 84-88 + "overTemperatureProtection" / Float32l, # 88-92 + "lowVoltageProtection" / Float32l, # 92-96 + "brightness" / Byte, # 96 + "volume" / Byte, # 97 + "metering" / Byte, # 98 + "outputCapacity" / Float32l, # 99-103 + "outputEnergy" / Float32l, # 103-107 + "outputClosedRaw" / Byte, # 107 + "protectionStateRaw" / Byte, # 108 + "modeRaw" / Byte, # 109 + "unknown1" / Byte, # 110 + "upperLimitVoltage" / Float32l, # 111-115 + "upperLimitCurrent" / Float32l, # 115-119 + "unknownVoltage" / Float32l, # 119-123 + "unknownCurrent" / Float32l, # 123-127 + "unknown2" / Float32l, # 127-131 + "unknown3" / Float32l, # 131-135 + "unknown4" / Float32l, # 135-139 + "extra" / GreedyBytes, # Any extra data + "meteringClosed" / Computed(lambda ctx: ctx.metering == 0), + "outputClosed" / Computed(lambda ctx: ctx.outputClosedRaw == 1), +) + diff --git a/dps150/packets/cmd.py b/dps150/packets/cmd.py new file mode 100644 index 0000000..38f4c25 --- /dev/null +++ b/dps150/packets/cmd.py @@ -0,0 +1,52 @@ +HEADER_OUT = 241 +HEADER_IN = 240 + +CMD_GET = 161 +CMD_BAUD = 176 +CMD_SET = 177 +CMD_SESSION = 193 + +# float +VOLTAGE_SET = 193 +CURRENT_SET = 194 + +GROUP1_VOLTAGE_SET = 197 +GROUP1_CURRENT_SET = 198 +GROUP2_VOLTAGE_SET = 199 +GROUP2_CURRENT_SET = 200 +GROUP3_VOLTAGE_SET = 201 +GROUP3_CURRENT_SET = 202 +GROUP4_VOLTAGE_SET = 203 +GROUP4_CURRENT_SET = 204 +GROUP5_VOLTAGE_SET = 205 +GROUP5_CURRENT_SET = 206 +GROUP6_VOLTAGE_SET = 207 +GROUP6_CURRENT_SET = 208 + +OVP = 209 +OCP = 210 +OPP = 211 +OTP = 212 +LVP = 213 + +METERING_ENABLE = 216 +OUTPUT_ENABLE = 219 + +# byte +BRIGHTNESS = 214 +VOLUME = 215 + +MODEL_NAME = 222 +HARDWARE_VERSION = 223 +FIRMWARE_VERSION = 224 +ALL = 255 + +PROTECTION_STATES = [ + "", + "OVP", + "OCP", + "OPP", + "OTP", + "LVP", + "REP", +] \ No newline at end of file diff --git a/main.py b/main.py new file mode 100755 index 0000000..67cd4c3 --- /dev/null +++ b/main.py @@ -0,0 +1,533 @@ +#!/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() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5cc1c2a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyserial +construct +matplotlib