first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.venv/
|
||||||
|
*/__pycache__/
|
||||||
382
dps150/__init__.py
Normal file
382
dps150/__init__.py
Normal file
@@ -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('<f', value) # Little-endian float32
|
||||||
|
self.send_command(c1, c2, c3, float_bytes)
|
||||||
|
|
||||||
|
def _send_command_raw(self, command: Union[bytes, bytearray]) -> 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)
|
||||||
BIN
dps150/packets/__pycache__/base.cpython-313.pyc
Normal file
BIN
dps150/packets/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
BIN
dps150/packets/__pycache__/cmd.cpython-313.pyc
Normal file
BIN
dps150/packets/__pycache__/cmd.cpython-313.pyc
Normal file
Binary file not shown.
86
dps150/packets/base.py
Normal file
86
dps150/packets/base.py
Normal file
@@ -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),
|
||||||
|
)
|
||||||
|
|
||||||
52
dps150/packets/cmd.py
Normal file
52
dps150/packets/cmd.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
533
main.py
Executable file
533
main.py
Executable file
@@ -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()
|
||||||
|
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pyserial
|
||||||
|
construct
|
||||||
|
matplotlib
|
||||||
Reference in New Issue
Block a user