tenma-ps 25.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tenma-ps might be problematic. Click here for more details.
- tenma_ps/__init__.py +0 -0
- tenma_ps/power_supply.py +195 -0
- tenma_ps/ps_fastapi.py +212 -0
- tenma_ps/ps_gui.py +403 -0
- tenma_ps-25.0.0.dist-info/METADATA +45 -0
- tenma_ps-25.0.0.dist-info/RECORD +7 -0
- tenma_ps-25.0.0.dist-info/WHEEL +4 -0
tenma_ps/__init__.py
ADDED
|
File without changes
|
tenma_ps/power_supply.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Type
|
|
3
|
+
|
|
4
|
+
import serial
|
|
5
|
+
import serial.tools.list_ports
|
|
6
|
+
from tenma import instantiate_tenma_class_from_device_response
|
|
7
|
+
|
|
8
|
+
logging.basicConfig(level=logging.INFO)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TenmaPs:
|
|
12
|
+
"""
|
|
13
|
+
Interface for a Tenma power supply device.
|
|
14
|
+
|
|
15
|
+
This class manages connection to a Tenma power supply, allowing users to
|
|
16
|
+
retrieve device status and control voltage/current settings.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
VOLTAGE_MULTIPLIER (float): Multiplier to convert volts to millivolts.
|
|
20
|
+
CURRENT_MULTIPLIER (float): Multiplier to convert amps to milliamps.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
from tenma_ps.power_supply import TenmaPs
|
|
25
|
+
|
|
26
|
+
with TenmaPs("COM4") as tenma_ps:
|
|
27
|
+
print("Device version:", tenma_ps.get_version())
|
|
28
|
+
print("Device status:", tenma_ps.get_status())
|
|
29
|
+
|
|
30
|
+
# Set voltage and current on channel 1
|
|
31
|
+
tenma_ps.set_voltage(channel=1, voltage=5.0)
|
|
32
|
+
tenma_ps.set_current(channel=1, current=1.0)
|
|
33
|
+
|
|
34
|
+
# Read voltage and current from channel 1
|
|
35
|
+
voltage = tenma_ps.read_voltage(channel=1)
|
|
36
|
+
current = tenma_ps.read_current(channel=1)
|
|
37
|
+
print(f"Channel 1 Voltage: {voltage} V")
|
|
38
|
+
print(f"Channel 1 Current: {current} A")
|
|
39
|
+
|
|
40
|
+
# Turn on and off the power supply
|
|
41
|
+
tenma_ps.turn_on()
|
|
42
|
+
print("Power supply turned ON.")
|
|
43
|
+
tenma_ps.turn_off()
|
|
44
|
+
print("Power supply turned OFF.")
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
VOLTAGE_MULTIPLIER: float = 1000.0 # To convert volts to millivolts
|
|
49
|
+
CURRENT_MULTIPLIER: float = 1000.0 # To convert amps to milliamps
|
|
50
|
+
|
|
51
|
+
def __init__(self, port: str) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Initialize the Tenma power supply interface.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
port (str): The COM port (e.g., "COM4") to which the device is connected.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
Exception: If the connection to the device fails.
|
|
60
|
+
"""
|
|
61
|
+
self._close_com_port_if_open(port)
|
|
62
|
+
self.device = instantiate_tenma_class_from_device_response(port)
|
|
63
|
+
|
|
64
|
+
def _close_com_port_if_open(self, port: str) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Close the COM port if it is already open.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
port (str): The COM port to check and close if open.
|
|
70
|
+
|
|
71
|
+
Logs:
|
|
72
|
+
Logs a message if the port is successfully closed.
|
|
73
|
+
Logs an error if there is an issue closing the port.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
for p in serial.tools.list_ports.comports():
|
|
77
|
+
if p.device == port:
|
|
78
|
+
try:
|
|
79
|
+
with serial.Serial(port) as ser:
|
|
80
|
+
if ser.is_open:
|
|
81
|
+
ser.close()
|
|
82
|
+
logging.info(f"Closed COM port: {port}")
|
|
83
|
+
except serial.SerialException as e:
|
|
84
|
+
logging.error(f"Error closing COM port {port}: {e}")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logging.error(f"Unexpected error closing COM port {port}: {e}")
|
|
87
|
+
|
|
88
|
+
def get_version(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Get the version information of the Tenma power supply.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str: The version string reported by the device.
|
|
94
|
+
"""
|
|
95
|
+
return self.device.getVersion()
|
|
96
|
+
|
|
97
|
+
def get_status(self) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Get the current status of the Tenma power supply.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
str: The status string reported by the device.
|
|
103
|
+
"""
|
|
104
|
+
return self.device.getStatus()
|
|
105
|
+
|
|
106
|
+
def turn_on(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Power on the Tenma power supply.
|
|
109
|
+
"""
|
|
110
|
+
self.device.ON()
|
|
111
|
+
|
|
112
|
+
def turn_off(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Power off the Tenma power supply.
|
|
115
|
+
"""
|
|
116
|
+
self.device.OFF()
|
|
117
|
+
|
|
118
|
+
def read_voltage(self, channel: int) -> float:
|
|
119
|
+
"""
|
|
120
|
+
Read the voltage from a specified channel.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
channel (int): The channel number to read from.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
float: The voltage value in volts.
|
|
127
|
+
"""
|
|
128
|
+
return self.device.runningVoltage(channel)
|
|
129
|
+
|
|
130
|
+
def read_current(self, channel: int) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Read the current from a specified channel.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
channel (int): The channel number to read from.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
float: The current value in amps.
|
|
139
|
+
"""
|
|
140
|
+
return self.device.runningCurrent(channel)
|
|
141
|
+
|
|
142
|
+
def set_voltage(self, channel: int, voltage: float) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Set the voltage for a specified channel.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
channel (int): The channel number to set.
|
|
148
|
+
voltage (float): The voltage value in volts.
|
|
149
|
+
"""
|
|
150
|
+
self.device.setVoltage(channel, voltage * self.VOLTAGE_MULTIPLIER)
|
|
151
|
+
|
|
152
|
+
def set_current(self, channel: int, current: float) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Set the current for a specified channel.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
channel (int): The channel number to set.
|
|
158
|
+
current (float): The current value in amps.
|
|
159
|
+
"""
|
|
160
|
+
self.device.setCurrent(channel, current * self.CURRENT_MULTIPLIER)
|
|
161
|
+
|
|
162
|
+
def close(self) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Close the connection to the Tenma power supply.
|
|
165
|
+
"""
|
|
166
|
+
self.device.close()
|
|
167
|
+
|
|
168
|
+
def __enter__(self) -> "TenmaPs":
|
|
169
|
+
"""
|
|
170
|
+
Enter the runtime context for the TenmaPs object.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
TenmaPs: The TenmaPs instance.
|
|
174
|
+
"""
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def __exit__(
|
|
178
|
+
self,
|
|
179
|
+
exc_type: Optional[Type[BaseException]],
|
|
180
|
+
exc_value: Optional[BaseException],
|
|
181
|
+
traceback: Optional[object]
|
|
182
|
+
) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Exit the runtime context and clean up resources.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
exc_type (Optional[Type[BaseException]]): Exception type, if any.
|
|
188
|
+
exc_value (Optional[BaseException]): Exception value, if any.
|
|
189
|
+
traceback (Optional[object]): Traceback object, if any.
|
|
190
|
+
"""
|
|
191
|
+
self.close()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
pass
|
tenma_ps/ps_fastapi.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from tenma_ps.power_supply import TenmaPs
|
|
7
|
+
|
|
8
|
+
app = FastAPI(
|
|
9
|
+
title="Tenma Power Supply API",
|
|
10
|
+
description=(
|
|
11
|
+
"API for controlling and monitoring a Tenma power supply via the [`TenmaPs`](power_supply.py) class. "
|
|
12
|
+
"See `/docs` for interactive documentation."
|
|
13
|
+
),
|
|
14
|
+
version="1.0.0"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Singleton device instance (for demo/simple use)
|
|
18
|
+
device: Optional[TenmaPs] = None
|
|
19
|
+
|
|
20
|
+
# ------------------- Pydantic Models -------------------
|
|
21
|
+
|
|
22
|
+
class ConnectRequest(BaseModel):
|
|
23
|
+
"""Request model for connecting to a COM port."""
|
|
24
|
+
port: str
|
|
25
|
+
|
|
26
|
+
class SetChannelValueRequest(BaseModel):
|
|
27
|
+
"""Request model for setting a single value (voltage or current) on a channel."""
|
|
28
|
+
channel: int
|
|
29
|
+
value: float
|
|
30
|
+
|
|
31
|
+
class SetVoltageCurrentRequest(BaseModel):
|
|
32
|
+
"""Request model for setting both voltage and current on a channel."""
|
|
33
|
+
channel: int
|
|
34
|
+
voltage: float
|
|
35
|
+
current: float
|
|
36
|
+
|
|
37
|
+
# ------------------- API Endpoints -------------------
|
|
38
|
+
|
|
39
|
+
@app.get("/", tags=["Info"])
|
|
40
|
+
def root():
|
|
41
|
+
"""
|
|
42
|
+
Root endpoint.
|
|
43
|
+
|
|
44
|
+
Returns links to API documentation.
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
"message": "Welcome to Tenma Power Supply API.",
|
|
48
|
+
"docs": "/docs",
|
|
49
|
+
"redoc": "/redoc"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@app.post("/connect", tags=["Connection"])
|
|
53
|
+
def connect(req: ConnectRequest):
|
|
54
|
+
"""
|
|
55
|
+
Connect to the Tenma power supply on the specified COM port.
|
|
56
|
+
|
|
57
|
+
- **port**: Serial port name (e.g., "COM4")
|
|
58
|
+
"""
|
|
59
|
+
global device
|
|
60
|
+
try:
|
|
61
|
+
device = TenmaPs(req.port)
|
|
62
|
+
return {"status": "connected", "port": req.port}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise HTTPException(status_code=500, detail=f"Failed to connect: {e}")
|
|
65
|
+
|
|
66
|
+
@app.post("/disconnect", tags=["Connection"])
|
|
67
|
+
def disconnect():
|
|
68
|
+
"""
|
|
69
|
+
Disconnect from the Tenma power supply.
|
|
70
|
+
"""
|
|
71
|
+
global device
|
|
72
|
+
if device is not None:
|
|
73
|
+
try:
|
|
74
|
+
device.close()
|
|
75
|
+
device = None
|
|
76
|
+
return {"status": "disconnected"}
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise HTTPException(status_code=500, detail=f"Failed to disconnect: {e}")
|
|
79
|
+
else:
|
|
80
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
81
|
+
|
|
82
|
+
@app.get("/version", tags=["Device Info"])
|
|
83
|
+
def get_version():
|
|
84
|
+
"""
|
|
85
|
+
Get the version information of the connected Tenma power supply.
|
|
86
|
+
"""
|
|
87
|
+
if device is None:
|
|
88
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
89
|
+
return {"version": device.get_version()}
|
|
90
|
+
|
|
91
|
+
@app.get("/status", tags=["Device Info"])
|
|
92
|
+
def get_status():
|
|
93
|
+
"""
|
|
94
|
+
Get the current status of the connected Tenma power supply.
|
|
95
|
+
"""
|
|
96
|
+
if device is None:
|
|
97
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
98
|
+
return {"status": device.get_status()}
|
|
99
|
+
|
|
100
|
+
@app.post("/turn_on", tags=["Output Control"])
|
|
101
|
+
def turn_on():
|
|
102
|
+
"""
|
|
103
|
+
Turn ON the power supply output.
|
|
104
|
+
"""
|
|
105
|
+
if device is None:
|
|
106
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
107
|
+
device.turn_on()
|
|
108
|
+
return {"output": "on"}
|
|
109
|
+
|
|
110
|
+
@app.post("/turn_off", tags=["Output Control"])
|
|
111
|
+
def turn_off():
|
|
112
|
+
"""
|
|
113
|
+
Turn OFF the power supply output.
|
|
114
|
+
"""
|
|
115
|
+
if device is None:
|
|
116
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
117
|
+
device.turn_off()
|
|
118
|
+
return {"output": "off"}
|
|
119
|
+
|
|
120
|
+
@app.get("/read_voltage", tags=["Read"])
|
|
121
|
+
def read_voltage(channel: int = Query(..., description="Channel number")):
|
|
122
|
+
"""
|
|
123
|
+
Read the voltage from a specified channel.
|
|
124
|
+
|
|
125
|
+
- **channel**: Channel number to read voltage from.
|
|
126
|
+
"""
|
|
127
|
+
if device is None:
|
|
128
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
129
|
+
voltage = device.read_voltage(channel)
|
|
130
|
+
return {"channel": channel, "voltage": voltage}
|
|
131
|
+
|
|
132
|
+
@app.get("/read_current", tags=["Read"])
|
|
133
|
+
def read_current(channel: int = Query(..., description="Channel number")):
|
|
134
|
+
"""
|
|
135
|
+
Read the current from a specified channel.
|
|
136
|
+
|
|
137
|
+
- **channel**: Channel number to read current from.
|
|
138
|
+
"""
|
|
139
|
+
if device is None:
|
|
140
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
141
|
+
current = device.read_current(channel)
|
|
142
|
+
return {"channel": channel, "current": current}
|
|
143
|
+
|
|
144
|
+
@app.post("/set_voltage", tags=["Set"])
|
|
145
|
+
def set_voltage(req: SetChannelValueRequest):
|
|
146
|
+
"""
|
|
147
|
+
Set the voltage for a specified channel.
|
|
148
|
+
|
|
149
|
+
- **channel**: Channel number to set voltage on.
|
|
150
|
+
- **value**: Voltage value in volts.
|
|
151
|
+
"""
|
|
152
|
+
if device is None:
|
|
153
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
154
|
+
device.set_voltage(req.channel, req.value)
|
|
155
|
+
return {"channel": req.channel, "voltage": req.value}
|
|
156
|
+
|
|
157
|
+
@app.post("/set_current", tags=["Set"])
|
|
158
|
+
def set_current(req: SetChannelValueRequest):
|
|
159
|
+
"""
|
|
160
|
+
Set the current for a specified channel.
|
|
161
|
+
|
|
162
|
+
- **channel**: Channel number to set current on.
|
|
163
|
+
- **value**: Current value in amps.
|
|
164
|
+
"""
|
|
165
|
+
if device is None:
|
|
166
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
167
|
+
device.set_current(req.channel, req.value)
|
|
168
|
+
return {"channel": req.channel, "current": req.value}
|
|
169
|
+
|
|
170
|
+
@app.post("/set_voltage_current", tags=["Set"])
|
|
171
|
+
def set_voltage_current(req: SetVoltageCurrentRequest):
|
|
172
|
+
"""
|
|
173
|
+
Set both voltage and current for a specified channel.
|
|
174
|
+
|
|
175
|
+
- **channel**: Channel number to set.
|
|
176
|
+
- **voltage**: Voltage value in volts.
|
|
177
|
+
- **current**: Current value in amps.
|
|
178
|
+
"""
|
|
179
|
+
if device is None:
|
|
180
|
+
raise HTTPException(status_code=400, detail="No device connected.")
|
|
181
|
+
device.set_voltage(req.channel, req.voltage)
|
|
182
|
+
device.set_current(req.channel, req.current)
|
|
183
|
+
return {"channel": req.channel, "voltage": req.voltage, "current": req.current}
|
|
184
|
+
|
|
185
|
+
# ------------------- Example Usage -------------------
|
|
186
|
+
"""
|
|
187
|
+
Example usage with HTTPie or curl:
|
|
188
|
+
|
|
189
|
+
1. Connect to device:
|
|
190
|
+
http POST http://localhost:8000/connect port="COM4"
|
|
191
|
+
|
|
192
|
+
2. Get device version:
|
|
193
|
+
http GET http://localhost:8000/version
|
|
194
|
+
|
|
195
|
+
3. Set voltage and current on channel 1:
|
|
196
|
+
http POST http://localhost:8000/set_voltage_current channel=1 voltage=5.0 current=1.0
|
|
197
|
+
|
|
198
|
+
4. Read voltage:
|
|
199
|
+
http GET http://localhost:8000/read_voltage channel==1
|
|
200
|
+
|
|
201
|
+
5. Turn ON output:
|
|
202
|
+
http POST http://localhost:8000/turn_on
|
|
203
|
+
|
|
204
|
+
6. Disconnect:
|
|
205
|
+
http POST http://localhost:8000/disconnect
|
|
206
|
+
|
|
207
|
+
Interactive API docs: http://localhost:8000/docs
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
import uvicorn
|
|
212
|
+
uvicorn.run("tenma_ps.ps_fastapi:app", host=socket.getfqdn(), reload=True)
|
tenma_ps/ps_gui.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import queue
|
|
4
|
+
import customtkinter as ctk
|
|
5
|
+
from tkinter import StringVar, IntVar
|
|
6
|
+
import serial.tools.list_ports
|
|
7
|
+
import sys
|
|
8
|
+
import signal
|
|
9
|
+
|
|
10
|
+
from tenma_ps.power_supply import TenmaPs
|
|
11
|
+
|
|
12
|
+
ctk.set_appearance_mode("dark")
|
|
13
|
+
ctk.set_default_color_theme("dark-blue")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PsGui(ctk.CTk):
|
|
17
|
+
"""
|
|
18
|
+
GUI for controlling and monitoring a Tenma power supply.
|
|
19
|
+
|
|
20
|
+
Features:
|
|
21
|
+
- Serial port selection and connection management
|
|
22
|
+
- Read device version
|
|
23
|
+
- Turn power supply ON/OFF
|
|
24
|
+
- Set voltage and current for a specific channel
|
|
25
|
+
- Live display of voltage and current for a selected channel
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""Initialize the GUI and set up all widgets and background threads."""
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.title("Tenma Power Supply GUI")
|
|
32
|
+
self.geometry("540x410")
|
|
33
|
+
self.resizable(False, False)
|
|
34
|
+
|
|
35
|
+
# Device state
|
|
36
|
+
self.device: TenmaPs | None = None
|
|
37
|
+
self.is_connected = False
|
|
38
|
+
self.is_monitoring = False
|
|
39
|
+
|
|
40
|
+
# Tkinter variables for UI state
|
|
41
|
+
self.selected_port = StringVar()
|
|
42
|
+
self.read_channel_var = IntVar(value=1)
|
|
43
|
+
self.live_voltage_var = StringVar(value="Voltage: -- V")
|
|
44
|
+
self.live_current_var = StringVar(value="Current: -- A")
|
|
45
|
+
self.device_version_var = StringVar(value="Version: --")
|
|
46
|
+
|
|
47
|
+
# For set voltage/current section
|
|
48
|
+
self.set_channel_var = IntVar(value=1)
|
|
49
|
+
self.set_voltage_var = StringVar(value="12.50")
|
|
50
|
+
self.set_current_var = StringVar(value="5.00")
|
|
51
|
+
|
|
52
|
+
# UI style
|
|
53
|
+
self._button_font = ctk.CTkFont(weight="bold", size=14)
|
|
54
|
+
self._button_text_color = "#ffffff"
|
|
55
|
+
|
|
56
|
+
# Threading/worker
|
|
57
|
+
self._action_queue = queue.Queue()
|
|
58
|
+
self._worker_thread = threading.Thread(target=self._process_action_queue, daemon=True)
|
|
59
|
+
self._worker_thread.start()
|
|
60
|
+
self._monitor_thread = None
|
|
61
|
+
|
|
62
|
+
self._build_gui()
|
|
63
|
+
|
|
64
|
+
# Ensure power supply is closed on exit/signals
|
|
65
|
+
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
66
|
+
signal.signal(signal.SIGINT, self._on_signal_exit)
|
|
67
|
+
signal.signal(signal.SIGTERM, self._on_signal_exit)
|
|
68
|
+
|
|
69
|
+
# ---------------- GUI Layout ----------------
|
|
70
|
+
|
|
71
|
+
def _build_gui(self):
|
|
72
|
+
"""Build and layout all GUI sections and widgets."""
|
|
73
|
+
|
|
74
|
+
# --- Serial Port Selection (all items in one row) ---
|
|
75
|
+
section_serial = ctk.CTkFrame(self, border_color="#1abc9c", border_width=2)
|
|
76
|
+
section_serial.pack(padx=15, pady=(15, 10), fill="x")
|
|
77
|
+
|
|
78
|
+
row_serial = ctk.CTkFrame(section_serial, fg_color="transparent")
|
|
79
|
+
row_serial.pack(padx=10, pady=10, fill="x")
|
|
80
|
+
|
|
81
|
+
ctk.CTkLabel(row_serial, text="Serial Port Selection:", font=ctk.CTkFont(weight="bold")).pack(side="left", padx=(0, 10))
|
|
82
|
+
self.combobox_ports = ctk.CTkComboBox(
|
|
83
|
+
row_serial, values=self._get_available_com_ports(), variable=self.selected_port, width=180
|
|
84
|
+
)
|
|
85
|
+
self.combobox_ports.pack(side="left", padx=(0, 10))
|
|
86
|
+
self.button_connect = ctk.CTkButton(
|
|
87
|
+
row_serial,
|
|
88
|
+
text="Connect",
|
|
89
|
+
command=lambda: self._enqueue_action(self._toggle_connection),
|
|
90
|
+
fg_color="#e74c3c",
|
|
91
|
+
font=self._button_font,
|
|
92
|
+
text_color=self._button_text_color,
|
|
93
|
+
hover_color="#ff7675"
|
|
94
|
+
)
|
|
95
|
+
self.button_connect.pack(side="left")
|
|
96
|
+
|
|
97
|
+
# --- Device Version section ---
|
|
98
|
+
section_version = ctk.CTkFrame(self, border_color="#2980b9", border_width=2)
|
|
99
|
+
section_version.pack(padx=15, pady=10, fill="x")
|
|
100
|
+
self.button_version = ctk.CTkButton(
|
|
101
|
+
section_version,
|
|
102
|
+
text="Read Version",
|
|
103
|
+
command=lambda: self._enqueue_action(self._read_device_version),
|
|
104
|
+
state="disabled",
|
|
105
|
+
font=self._button_font,
|
|
106
|
+
text_color=self._button_text_color,
|
|
107
|
+
fg_color="#2980b9",
|
|
108
|
+
hover_color="#74b9ff"
|
|
109
|
+
)
|
|
110
|
+
self.button_version.pack(padx=10, pady=10, side="left")
|
|
111
|
+
self.label_version = ctk.CTkLabel(
|
|
112
|
+
section_version, textvariable=self.device_version_var, font=ctk.CTkFont(size=14, weight="bold")
|
|
113
|
+
)
|
|
114
|
+
self.label_version.pack(padx=10, pady=10, side="left")
|
|
115
|
+
|
|
116
|
+
# --- Power Control section (Turn ON/OFF) ---
|
|
117
|
+
section_power = ctk.CTkFrame(self, border_color="#9b59b6", border_width=2)
|
|
118
|
+
section_power.pack(padx=15, pady=10, fill="x")
|
|
119
|
+
|
|
120
|
+
power_row = ctk.CTkFrame(section_power, fg_color="transparent")
|
|
121
|
+
power_row.pack(padx=10, pady=10, fill="x")
|
|
122
|
+
|
|
123
|
+
self.button_on = ctk.CTkButton(
|
|
124
|
+
power_row,
|
|
125
|
+
text="Turn ON",
|
|
126
|
+
command=lambda: self._enqueue_action(self._turn_output_on),
|
|
127
|
+
state="disabled",
|
|
128
|
+
fg_color="#27ae60",
|
|
129
|
+
font=self._button_font,
|
|
130
|
+
text_color=self._button_text_color,
|
|
131
|
+
hover_color="#00ff99",
|
|
132
|
+
width=200,
|
|
133
|
+
height=36
|
|
134
|
+
)
|
|
135
|
+
self.button_on.pack(side="left", padx=(0, 20), fill="x", expand=True)
|
|
136
|
+
self.button_off = ctk.CTkButton(
|
|
137
|
+
power_row,
|
|
138
|
+
text="Turn OFF",
|
|
139
|
+
command=lambda: self._enqueue_action(self._turn_output_off),
|
|
140
|
+
state="disabled",
|
|
141
|
+
fg_color="#c0392b",
|
|
142
|
+
font=self._button_font,
|
|
143
|
+
text_color=self._button_text_color,
|
|
144
|
+
hover_color="#ff7675",
|
|
145
|
+
width=200,
|
|
146
|
+
height=36
|
|
147
|
+
)
|
|
148
|
+
self.button_off.pack(side="left", padx=(0, 0), fill="x", expand=True)
|
|
149
|
+
|
|
150
|
+
# --- Set Voltage/Current for Channel section ---
|
|
151
|
+
section_set = ctk.CTkFrame(self, border_color="#e67e22", border_width=2)
|
|
152
|
+
section_set.pack(padx=15, pady=10, fill="x")
|
|
153
|
+
|
|
154
|
+
set_row = ctk.CTkFrame(section_set, fg_color="transparent")
|
|
155
|
+
set_row.pack(padx=10, pady=15, fill="x")
|
|
156
|
+
|
|
157
|
+
ctk.CTkLabel(set_row, text="Channel:", font=ctk.CTkFont(weight="bold")).pack(side="left")
|
|
158
|
+
self.set_channel_entry = ctk.CTkEntry(
|
|
159
|
+
set_row, textvariable=self.set_channel_var, width=40, font=ctk.CTkFont(weight="bold"), state="disabled"
|
|
160
|
+
)
|
|
161
|
+
self.set_channel_entry.pack(side="left", padx=(5, 15))
|
|
162
|
+
|
|
163
|
+
ctk.CTkLabel(set_row, text="Voltage (V):", font=ctk.CTkFont(weight="bold")).pack(side="left")
|
|
164
|
+
self.set_voltage_entry = ctk.CTkEntry(
|
|
165
|
+
set_row, textvariable=self.set_voltage_var, width=60, font=ctk.CTkFont(weight="bold"), state="disabled"
|
|
166
|
+
)
|
|
167
|
+
self.set_voltage_entry.pack(side="left", padx=(5, 15))
|
|
168
|
+
|
|
169
|
+
ctk.CTkLabel(set_row, text="Current (A):", font=ctk.CTkFont(weight="bold")).pack(side="left")
|
|
170
|
+
self.set_current_entry = ctk.CTkEntry(
|
|
171
|
+
set_row, textvariable=self.set_current_var, width=60, font=ctk.CTkFont(weight="bold"), state="disabled"
|
|
172
|
+
)
|
|
173
|
+
self.set_current_entry.pack(side="left", padx=(5, 15))
|
|
174
|
+
|
|
175
|
+
self.button_set = ctk.CTkButton(
|
|
176
|
+
set_row,
|
|
177
|
+
text="Set",
|
|
178
|
+
command=lambda: self._enqueue_action(self._set_channel_voltage_current),
|
|
179
|
+
state="disabled",
|
|
180
|
+
fg_color="#16a085",
|
|
181
|
+
font=self._button_font,
|
|
182
|
+
text_color=self._button_text_color,
|
|
183
|
+
hover_color="#1abc9c"
|
|
184
|
+
)
|
|
185
|
+
self.button_set.pack(side="left", padx=(10, 0))
|
|
186
|
+
|
|
187
|
+
# --- Live Voltage/Current Reading section ---
|
|
188
|
+
section_read = ctk.CTkFrame(self, border_color="#34495e", border_width=2)
|
|
189
|
+
section_read.pack(padx=15, pady=10, fill="x")
|
|
190
|
+
|
|
191
|
+
row_read = ctk.CTkFrame(section_read, fg_color="transparent")
|
|
192
|
+
row_read.pack(padx=10, pady=15, fill="x")
|
|
193
|
+
|
|
194
|
+
ctk.CTkLabel(row_read, text="Channel:", font=ctk.CTkFont(weight="bold")).pack(side="left")
|
|
195
|
+
self.read_channel_entry = ctk.CTkEntry(
|
|
196
|
+
row_read, textvariable=self.read_channel_var, width=40, font=ctk.CTkFont(weight="bold")
|
|
197
|
+
)
|
|
198
|
+
self.read_channel_entry.pack(side="left", padx=(5, 20))
|
|
199
|
+
|
|
200
|
+
self.label_voltage = ctk.CTkLabel(
|
|
201
|
+
row_read, textvariable=self.live_voltage_var, font=ctk.CTkFont(size=16, weight="bold")
|
|
202
|
+
)
|
|
203
|
+
self.label_voltage.pack(side="left", padx=(0, 20))
|
|
204
|
+
self.label_current = ctk.CTkLabel(
|
|
205
|
+
row_read, textvariable=self.live_current_var, font=ctk.CTkFont(size=16, weight="bold")
|
|
206
|
+
)
|
|
207
|
+
self.label_current.pack(side="left")
|
|
208
|
+
|
|
209
|
+
# ---------------- Serial Port Helpers ----------------
|
|
210
|
+
|
|
211
|
+
def _get_available_com_ports(self):
|
|
212
|
+
"""Return a list of available COM port device names."""
|
|
213
|
+
return [p.device for p in serial.tools.list_ports.comports()]
|
|
214
|
+
|
|
215
|
+
# ---------------- Action Queue/Worker ----------------
|
|
216
|
+
|
|
217
|
+
def _enqueue_action(self, func):
|
|
218
|
+
"""Add a function to the action queue to be executed in the worker thread."""
|
|
219
|
+
self._action_queue.put(func)
|
|
220
|
+
|
|
221
|
+
def _process_action_queue(self):
|
|
222
|
+
"""Continuously process actions from the queue in a background thread."""
|
|
223
|
+
while True:
|
|
224
|
+
func = self._action_queue.get()
|
|
225
|
+
try:
|
|
226
|
+
func()
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self._show_error_mainthread(f"Error: {e}")
|
|
229
|
+
self._action_queue.task_done()
|
|
230
|
+
|
|
231
|
+
# ---------------- Device Actions ----------------
|
|
232
|
+
|
|
233
|
+
def _toggle_connection(self):
|
|
234
|
+
"""Connect or disconnect from the power supply based on current state."""
|
|
235
|
+
if not self.is_connected:
|
|
236
|
+
port = self.selected_port.get()
|
|
237
|
+
if not port:
|
|
238
|
+
self._show_error_mainthread("Please select a COM port.")
|
|
239
|
+
return
|
|
240
|
+
try:
|
|
241
|
+
self.device = TenmaPs(port)
|
|
242
|
+
self.is_connected = True
|
|
243
|
+
self._after_mainthread(self._on_connect_success)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
self._show_error_mainthread(f"Failed to connect: {e}")
|
|
246
|
+
else:
|
|
247
|
+
self._disconnect_device()
|
|
248
|
+
|
|
249
|
+
def _on_connect_success(self):
|
|
250
|
+
"""Update UI after successful connection."""
|
|
251
|
+
self.button_connect.configure(text="Disconnect", fg_color="#27ae60")
|
|
252
|
+
self.combobox_ports.configure(state="disabled")
|
|
253
|
+
self.button_on.configure(state="normal")
|
|
254
|
+
self.button_off.configure(state="normal")
|
|
255
|
+
self.button_version.configure(state="normal")
|
|
256
|
+
self.button_set.configure(state="normal")
|
|
257
|
+
self.set_channel_entry.configure(state="normal")
|
|
258
|
+
self.set_voltage_entry.configure(state="normal")
|
|
259
|
+
self.set_current_entry.configure(state="normal")
|
|
260
|
+
self._start_monitoring()
|
|
261
|
+
|
|
262
|
+
def _disconnect_device(self):
|
|
263
|
+
"""Disconnect from the power supply and reset UI."""
|
|
264
|
+
self._stop_monitoring()
|
|
265
|
+
if self.device:
|
|
266
|
+
try:
|
|
267
|
+
self.device.close()
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
self.device = None
|
|
271
|
+
self.is_connected = False
|
|
272
|
+
self.button_connect.configure(text="Connect", fg_color="#e74c3c")
|
|
273
|
+
self.combobox_ports.configure(state="normal")
|
|
274
|
+
self.button_on.configure(state="disabled")
|
|
275
|
+
self.button_off.configure(state="disabled")
|
|
276
|
+
self.button_version.configure(state="disabled")
|
|
277
|
+
self.button_set.configure(state="disabled")
|
|
278
|
+
self.set_channel_entry.configure(state="disabled")
|
|
279
|
+
self.set_voltage_entry.configure(state="disabled")
|
|
280
|
+
self.set_current_entry.configure(state="disabled")
|
|
281
|
+
self.live_voltage_var.set("Voltage: -- V")
|
|
282
|
+
self.live_current_var.set("Current: -- A")
|
|
283
|
+
self.device_version_var.set("Version: --")
|
|
284
|
+
|
|
285
|
+
def _start_monitoring(self):
|
|
286
|
+
"""Start background thread to monitor live voltage and current."""
|
|
287
|
+
self.is_monitoring = True
|
|
288
|
+
self._monitor_thread = threading.Thread(target=self._monitor_live_values, daemon=True)
|
|
289
|
+
self._monitor_thread.start()
|
|
290
|
+
|
|
291
|
+
def _stop_monitoring(self):
|
|
292
|
+
"""Stop the background monitoring thread."""
|
|
293
|
+
self.is_monitoring = False
|
|
294
|
+
if self._monitor_thread and self._monitor_thread.is_alive():
|
|
295
|
+
self._monitor_thread.join(timeout=1)
|
|
296
|
+
|
|
297
|
+
def _monitor_live_values(self):
|
|
298
|
+
"""Continuously read voltage and current from the selected channel and update the UI."""
|
|
299
|
+
while self.is_monitoring and self.device:
|
|
300
|
+
try:
|
|
301
|
+
channel = self.read_channel_var.get()
|
|
302
|
+
voltage = self.device.read_voltage(channel=channel)
|
|
303
|
+
current = self.device.read_current(channel=channel)
|
|
304
|
+
self._after_mainthread(lambda: self.live_voltage_var.set(f"Voltage: {voltage:.2f} V"))
|
|
305
|
+
self._after_mainthread(lambda: self.live_current_var.set(f"Current: {current:.2f} A"))
|
|
306
|
+
except Exception:
|
|
307
|
+
self._after_mainthread(lambda: self.live_voltage_var.set("Voltage: -- V"))
|
|
308
|
+
self._after_mainthread(lambda: self.live_current_var.set("Current: -- A"))
|
|
309
|
+
time.sleep(1)
|
|
310
|
+
|
|
311
|
+
def _turn_output_on(self):
|
|
312
|
+
"""Turn ON the power supply output."""
|
|
313
|
+
if self.device:
|
|
314
|
+
try:
|
|
315
|
+
self.device.turn_on()
|
|
316
|
+
self._after_mainthread(lambda: self.button_on.configure(state="disabled"))
|
|
317
|
+
self._after_mainthread(lambda: self.button_off.configure(state="normal"))
|
|
318
|
+
except Exception as e:
|
|
319
|
+
self._show_error_mainthread(f"Failed to turn ON: {e}")
|
|
320
|
+
|
|
321
|
+
def _turn_output_off(self):
|
|
322
|
+
"""Turn OFF the power supply output."""
|
|
323
|
+
if self.device:
|
|
324
|
+
try:
|
|
325
|
+
self.device.turn_off()
|
|
326
|
+
self._after_mainthread(lambda: self.button_on.configure(state="normal"))
|
|
327
|
+
self._after_mainthread(lambda: self.button_off.configure(state="disabled"))
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self._show_error_mainthread(f"Failed to turn OFF: {e}")
|
|
330
|
+
|
|
331
|
+
def _read_device_version(self):
|
|
332
|
+
"""Read and display the device version."""
|
|
333
|
+
if self.device:
|
|
334
|
+
try:
|
|
335
|
+
version = self.device.get_version()
|
|
336
|
+
self._after_mainthread(lambda: self.device_version_var.set(f"Version: {version}"))
|
|
337
|
+
except Exception as e:
|
|
338
|
+
self._after_mainthread(lambda: self.device_version_var.set("Version: --"))
|
|
339
|
+
self._show_error_mainthread(f"Failed to read version: {e}")
|
|
340
|
+
|
|
341
|
+
def _set_channel_voltage_current(self):
|
|
342
|
+
"""Set voltage and current for the specified channel using the set section."""
|
|
343
|
+
if self.device:
|
|
344
|
+
try:
|
|
345
|
+
channel = self.set_channel_var.get()
|
|
346
|
+
voltage_input = self.set_voltage_var.get()
|
|
347
|
+
current_input = self.set_current_var.get()
|
|
348
|
+
|
|
349
|
+
# Validate voltage and current inputs
|
|
350
|
+
try:
|
|
351
|
+
voltage = float(voltage_input)
|
|
352
|
+
except ValueError:
|
|
353
|
+
raise ValueError(f"Invalid voltage input: '{voltage_input}'")
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
current = float(current_input)
|
|
357
|
+
except ValueError:
|
|
358
|
+
raise ValueError(f"Invalid current input: '{current_input}'")
|
|
359
|
+
|
|
360
|
+
self.device.set_voltage(channel=channel, voltage=voltage)
|
|
361
|
+
self.device.set_current(channel=channel, current=current)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self._show_error_mainthread(f"Failed to set voltage/current: {e}")
|
|
364
|
+
|
|
365
|
+
# ---------------- Thread-safe GUI update helpers ----------------
|
|
366
|
+
|
|
367
|
+
def _after_mainthread(self, func):
|
|
368
|
+
"""Schedule a function to run on the main thread."""
|
|
369
|
+
self.after(0, func)
|
|
370
|
+
|
|
371
|
+
def _show_error_mainthread(self, message):
|
|
372
|
+
"""Show an error message in a thread-safe way."""
|
|
373
|
+
self._after_mainthread(lambda: self._show_error_popup(message))
|
|
374
|
+
|
|
375
|
+
def _show_error_popup(self, message):
|
|
376
|
+
"""Display an error message in a popup window."""
|
|
377
|
+
error_win = ctk.CTkToplevel(self)
|
|
378
|
+
error_win.title("Error")
|
|
379
|
+
error_win.geometry("300x100")
|
|
380
|
+
ctk.CTkLabel(error_win, text=message, text_color="red", font=ctk.CTkFont(weight="bold", size=13)).pack(padx=20, pady=20)
|
|
381
|
+
ctk.CTkButton(error_win, text="OK", command=error_win.destroy, font=self._button_font, text_color=self._button_text_color).pack(pady=(0, 10))
|
|
382
|
+
|
|
383
|
+
# ---------------- Cleanup/Exit ----------------
|
|
384
|
+
|
|
385
|
+
def _on_close(self):
|
|
386
|
+
"""Handle GUI close event and ensure power supply is closed."""
|
|
387
|
+
self._stop_monitoring()
|
|
388
|
+
if self.device:
|
|
389
|
+
try:
|
|
390
|
+
self.device.close()
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
self.destroy()
|
|
394
|
+
|
|
395
|
+
def _on_signal_exit(self, signum, frame):
|
|
396
|
+
"""Handle process signals to ensure power supply is closed."""
|
|
397
|
+
self._on_close()
|
|
398
|
+
sys.exit(0)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
app = PsGui()
|
|
403
|
+
app.mainloop()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tenma-ps
|
|
3
|
+
Version: 25.0.0
|
|
4
|
+
Summary: python package for controlling tenma power supply
|
|
5
|
+
Keywords: python,tenma,power,supply,application
|
|
6
|
+
Author: chaitu-ycr
|
|
7
|
+
Author-email: chaitu-ycr <chaitu.ycr@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 chaitu-ycr
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
32
|
+
Requires-Dist: tenma-serial
|
|
33
|
+
Requires-Dist: customtkinter
|
|
34
|
+
Requires-Dist: fastapi[all]
|
|
35
|
+
Requires-Python: >=3.10, <=3.14
|
|
36
|
+
Project-URL: documentation, https://chaitu-ycr.github.io/automotive-test-kit/packages/tenma_ps
|
|
37
|
+
Project-URL: homepage, https://github.com/chaitu-ycr/automotive-test-kit
|
|
38
|
+
Project-URL: repository, https://github.com/chaitu-ycr/automotive-test-kit
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# tenma_ps
|
|
42
|
+
|
|
43
|
+
python package for controlling tenma power supply
|
|
44
|
+
|
|
45
|
+
## [source manual](https://chaitu-ycr.github.io/automotive-test-kit/packages/tenma_ps/#source-manual)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tenma_ps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tenma_ps/power_supply.py,sha256=sdRl9zoONHmR9nraouvBYlAjSzrP3bFbgRSiMetFFRc,6119
|
|
3
|
+
tenma_ps/ps_fastapi.py,sha256=fM-lBjxFCiqdqGdbnrwdBtKxvziW_cegs8iv_-OM5YM,6649
|
|
4
|
+
tenma_ps/ps_gui.py,sha256=QIZ1w3mExz7dUdl7H21DDSHgR5tQc7mAqwzxa4Eg5CY,17033
|
|
5
|
+
tenma_ps-25.0.0.dist-info/WHEEL,sha256=5h_Q-_6zWQhhADpsAD_Xpw7gFbCRK5WjOOEq0nB806Q,79
|
|
6
|
+
tenma_ps-25.0.0.dist-info/METADATA,sha256=PxhqqQa3_Hapfdvu2XQ-UhPn6DpPhuiiSDSq9gLjvlY,2206
|
|
7
|
+
tenma_ps-25.0.0.dist-info/RECORD,,
|