kmtronic-usb-relay 26.0.1__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.
- kmtronic_usb_relay/__init__.py +0 -0
- kmtronic_usb_relay/com_utils.py +303 -0
- kmtronic_usb_relay/four_channel_relay.py +229 -0
- kmtronic_usb_relay/four_channel_relay_app.py +500 -0
- kmtronic_usb_relay/four_channel_relay_gui.py +324 -0
- kmtronic_usb_relay-26.0.1.dist-info/METADATA +64 -0
- kmtronic_usb_relay-26.0.1.dist-info/RECORD +8 -0
- kmtronic_usb_relay-26.0.1.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
import serial
|
|
5
|
+
import serial.tools.list_ports
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class SerialComUtils:
|
|
10
|
+
"""
|
|
11
|
+
User-friendly utility class for serial COM port communication.
|
|
12
|
+
|
|
13
|
+
This class provides methods for discovering available serial ports,
|
|
14
|
+
connecting/disconnecting to a port, and sending/receiving data.
|
|
15
|
+
It also supports context management for safe resource handling.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# --- Initialization and Configuration ---
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
baudrate: int = 9600,
|
|
23
|
+
bytesize: int = serial.EIGHTBITS,
|
|
24
|
+
stopbits: int = serial.STOPBITS_ONE,
|
|
25
|
+
parity: str = serial.PARITY_NONE,
|
|
26
|
+
timeout: Optional[float] = 2.5,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize SerialComUtils with serial parameters.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
baudrate (int): Serial baudrate (default: 9600).
|
|
33
|
+
bytesize (int): Number of data bits (default: serial.EIGHTBITS).
|
|
34
|
+
stopbits (int): Number of stop bits (default: serial.STOPBITS_ONE).
|
|
35
|
+
parity (str): Parity setting (default: serial.PARITY_NONE).
|
|
36
|
+
timeout (float, optional): Read timeout in seconds, or None for blocking mode (default: 2.5).
|
|
37
|
+
"""
|
|
38
|
+
self.connection: Optional[serial.Serial] = None
|
|
39
|
+
self.baudrate = baudrate
|
|
40
|
+
self.bytesize = bytesize
|
|
41
|
+
self.stopbits = stopbits
|
|
42
|
+
self.parity = parity
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
|
|
45
|
+
def get_connection_params(self) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Get the current serial connection parameters.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
dict: Dictionary containing baudrate, bytesize, stopbits, parity, and timeout.
|
|
51
|
+
"""
|
|
52
|
+
return {
|
|
53
|
+
"baudrate": self.baudrate,
|
|
54
|
+
"bytesize": self.bytesize,
|
|
55
|
+
"stopbits": self.stopbits,
|
|
56
|
+
"parity": self.parity,
|
|
57
|
+
"timeout": self.timeout,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Return a string representation of the SerialComUtils instance.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: Human-readable summary of the current configuration and port.
|
|
66
|
+
"""
|
|
67
|
+
port = self.connection.port if self.connection and self.connection.is_open else None
|
|
68
|
+
return (
|
|
69
|
+
f"<SerialComUtils(port={port}, baudrate={self.baudrate}, "
|
|
70
|
+
f"bytesize={self.bytesize}, stopbits={self.stopbits}, "
|
|
71
|
+
f"parity={self.parity}, timeout={self.timeout})>"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# --- Port Discovery and Info ---
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_port_names() -> List[str]:
|
|
78
|
+
"""
|
|
79
|
+
List all available COM port device names.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List[str]: List of device names (e.g., ['COM1', 'COM2']).
|
|
83
|
+
"""
|
|
84
|
+
return [port.device for port in serial.tools.list_ports.comports()]
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def get_port_details() -> List[Dict[str, Any]]:
|
|
88
|
+
"""
|
|
89
|
+
Get details of all available COM ports.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List[dict]: Each dict contains:
|
|
93
|
+
- device (str): COM port number (e.g., 'COM1')
|
|
94
|
+
- description (str): Port description
|
|
95
|
+
- hwid (str): Hardware ID
|
|
96
|
+
"""
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
"device": port.device,
|
|
100
|
+
"description": port.description,
|
|
101
|
+
"hwid": port.hwid
|
|
102
|
+
}
|
|
103
|
+
for port in serial.tools.list_ports.comports()
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def format_port_detail(port_info: Dict[str, Any]) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Format a port info dictionary as a readable string.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
port_info (dict): Dictionary with keys 'device', 'description', 'hwid'.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: Formatted string describing the port.
|
|
116
|
+
"""
|
|
117
|
+
return (
|
|
118
|
+
f"Device: {port_info['device']}, "
|
|
119
|
+
f"Description: {port_info['description']}, "
|
|
120
|
+
f"HWID: {port_info['hwid']}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def log_port_details(port_name: Optional[str] = None) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Log details of all COM ports or a specific port if port_name is given.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
port_name (str, optional): Device name of the port (e.g., 'COM3').
|
|
130
|
+
"""
|
|
131
|
+
ports = SerialComUtils.get_port_details()
|
|
132
|
+
if port_name:
|
|
133
|
+
for port in ports:
|
|
134
|
+
if port["device"] == port_name:
|
|
135
|
+
logger.info(SerialComUtils.format_port_detail(port))
|
|
136
|
+
return
|
|
137
|
+
logger.warning(f"Port {port_name} not found.")
|
|
138
|
+
else:
|
|
139
|
+
for port in ports:
|
|
140
|
+
logger.info(SerialComUtils.format_port_detail(port))
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def get_busy_ports() -> List[str]:
|
|
144
|
+
"""
|
|
145
|
+
Attempt to list COM ports that are currently in use (best effort).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List[str]: List of port device names that are in use.
|
|
149
|
+
"""
|
|
150
|
+
busy_ports = []
|
|
151
|
+
for port in SerialComUtils.get_port_names():
|
|
152
|
+
try:
|
|
153
|
+
with serial.Serial(port) as s:
|
|
154
|
+
pass
|
|
155
|
+
except (OSError, serial.SerialException):
|
|
156
|
+
busy_ports.append(port)
|
|
157
|
+
return busy_ports
|
|
158
|
+
|
|
159
|
+
# --- Connection Management ---
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def is_connected(self) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Check if the serial connection is open.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
bool: True if connected, False otherwise.
|
|
168
|
+
"""
|
|
169
|
+
return self.connection is not None and self.connection.is_open
|
|
170
|
+
|
|
171
|
+
def connect(self, port: str) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Open a connection to a COM port with the configured parameters.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
port (str): Device name of the port (e.g., 'COM3').
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
bool: True if connection was successful, False otherwise.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
self.connection = serial.Serial(
|
|
183
|
+
port=port,
|
|
184
|
+
baudrate=self.baudrate,
|
|
185
|
+
bytesize=self.bytesize,
|
|
186
|
+
stopbits=self.stopbits,
|
|
187
|
+
parity=self.parity,
|
|
188
|
+
timeout=self.timeout
|
|
189
|
+
)
|
|
190
|
+
logger.info(f"Connected to {port}")
|
|
191
|
+
return True
|
|
192
|
+
except serial.SerialException as e:
|
|
193
|
+
logger.error(f"Failed to connect to {port}: {e}")
|
|
194
|
+
self.connection = None
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def open_connection(self, port: str) -> bool:
|
|
198
|
+
"""
|
|
199
|
+
Alias for connect. Opens a connection to the specified port.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
port (str): Device name of the port.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
bool: True if connection was successful, False otherwise.
|
|
206
|
+
"""
|
|
207
|
+
return self.connect(port)
|
|
208
|
+
|
|
209
|
+
def disconnect(self) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Alias for close_connection. Closes the serial connection.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
bool: True if disconnected or already closed, False if error occurred.
|
|
215
|
+
"""
|
|
216
|
+
return self.close_connection()
|
|
217
|
+
|
|
218
|
+
def close_connection(self) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Close the connection to the COM port.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
bool: True if disconnected or already closed, False if error occurred.
|
|
224
|
+
"""
|
|
225
|
+
if self.connection and self.connection.is_open:
|
|
226
|
+
try:
|
|
227
|
+
self.connection.close()
|
|
228
|
+
logger.info("Disconnected.")
|
|
229
|
+
self.connection = None
|
|
230
|
+
return True
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error while disconnecting: {e}")
|
|
233
|
+
return False
|
|
234
|
+
self.connection = None
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# --- Data Transfer ---
|
|
238
|
+
|
|
239
|
+
def send(self, data: bytes) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Send data to the serial port.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
data (bytes): Bytes to send.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
bool: True if sent, False if not connected.
|
|
248
|
+
"""
|
|
249
|
+
if self.is_connected:
|
|
250
|
+
self.connection.write(data)
|
|
251
|
+
return True
|
|
252
|
+
else:
|
|
253
|
+
logger.warning("No open connection. Cannot send data.")
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def receive(self, size: int = 1, as_int_list: bool = False) -> Optional[Union[bytes, List[int]]]:
|
|
257
|
+
"""
|
|
258
|
+
Receive data from the serial port.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
size (int): Number of bytes to read (default: 1).
|
|
262
|
+
as_int_list (bool): If True, return a list of ints instead of bytes.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
bytes or List[int] or None: Received data, or None if not connected.
|
|
266
|
+
"""
|
|
267
|
+
if self.is_connected:
|
|
268
|
+
data = self.connection.read(size)
|
|
269
|
+
if as_int_list:
|
|
270
|
+
return list(data)
|
|
271
|
+
return data
|
|
272
|
+
else:
|
|
273
|
+
logger.warning("No open connection. Cannot receive data.")
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
# --- Context Management ---
|
|
277
|
+
|
|
278
|
+
def __enter__(self) -> "SerialComUtils":
|
|
279
|
+
"""
|
|
280
|
+
Enter the runtime context related to this object.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
SerialComUtils: The instance itself.
|
|
284
|
+
"""
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
def __exit__(
|
|
288
|
+
self,
|
|
289
|
+
exc_type: Optional[type],
|
|
290
|
+
exc_val: Optional[BaseException],
|
|
291
|
+
exc_tb: Optional[object],
|
|
292
|
+
) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Exit the runtime context and close the serial connection.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
exc_type: Exception type.
|
|
298
|
+
exc_val: Exception value.
|
|
299
|
+
exc_tb: Exception traceback.
|
|
300
|
+
"""
|
|
301
|
+
self.close_connection()
|
|
302
|
+
|
|
303
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from kmtronic_usb_relay.com_utils import SerialComUtils
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class RelayController:
|
|
10
|
+
"""Controller for KMTronic 4-Channel USB Relay devices.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
with RelayController("COM4") as relay:
|
|
14
|
+
relay.turn_on(1)
|
|
15
|
+
relay.turn_off(2)
|
|
16
|
+
print(relay.statuses)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
RELAY_COUNT = 4
|
|
20
|
+
STATUS_ON = "ON"
|
|
21
|
+
STATUS_OFF = "OFF"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
com_port: str,
|
|
26
|
+
switch_delay: float = 1.0,
|
|
27
|
+
serial_utils: Optional[SerialComUtils] = None,
|
|
28
|
+
auto_connect: bool = True,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the RelayController.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
com_port (str): The COM port to which the relay board is connected (e.g., 'COM4').
|
|
35
|
+
switch_delay (float): Delay in seconds after switching a relay (default: 1.0).
|
|
36
|
+
serial_utils (Optional[SerialComUtils]): Custom SerialComUtils instance for testing/mocking.
|
|
37
|
+
auto_connect (bool): Automatically open connection on init (default: True).
|
|
38
|
+
"""
|
|
39
|
+
self.com_port = com_port
|
|
40
|
+
self.switch_delay = switch_delay
|
|
41
|
+
self.serial_utils = serial_utils or SerialComUtils()
|
|
42
|
+
self._is_connected = False
|
|
43
|
+
if auto_connect:
|
|
44
|
+
self.connect()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_connected(self) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if the relay controller is connected to the COM port.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
bool: True if connected, False otherwise.
|
|
53
|
+
"""
|
|
54
|
+
return self.serial_utils.is_connected if self.serial_utils else False
|
|
55
|
+
|
|
56
|
+
def connect(self) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Open the serial connection to the relay board.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
Exception: If the connection cannot be established.
|
|
62
|
+
"""
|
|
63
|
+
if not self.is_connected:
|
|
64
|
+
try:
|
|
65
|
+
self._is_connected = self.serial_utils.open_connection(self.com_port)
|
|
66
|
+
logger.info(f"Connected to relay on {self.com_port}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to open serial connection on {self.com_port}: {e}")
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Close the serial connection to the relay board.
|
|
74
|
+
|
|
75
|
+
This method is safe to call multiple times.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
result = self.serial_utils.close_connection()
|
|
79
|
+
self._is_connected = not result
|
|
80
|
+
logger.info("Serial connection closed.")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning(f"Error closing serial connection: {e}")
|
|
83
|
+
|
|
84
|
+
def turn_on(self, relay_number: int) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Turn ON the specified relay.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
relay_number (int): Relay number to turn ON (1-4).
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If relay_number is out of range.
|
|
93
|
+
Exception: If sending the command fails.
|
|
94
|
+
"""
|
|
95
|
+
self._validate_relay_number(relay_number)
|
|
96
|
+
self._send_relay_command(relay_number, True)
|
|
97
|
+
|
|
98
|
+
def turn_off(self, relay_number: int) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Turn OFF the specified relay.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
relay_number (int): Relay number to turn OFF (1-4).
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If relay_number is out of range.
|
|
107
|
+
Exception: If sending the command fails.
|
|
108
|
+
"""
|
|
109
|
+
self._validate_relay_number(relay_number)
|
|
110
|
+
self._send_relay_command(relay_number, False)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def statuses(self) -> Dict[str, str]:
|
|
114
|
+
"""
|
|
115
|
+
Get the status of all relays as a dictionary.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict[str, str]: Dictionary mapping relay names (e.g., 'R1') to their status ('ON' or 'OFF').
|
|
119
|
+
"""
|
|
120
|
+
return self.get_statuses()
|
|
121
|
+
|
|
122
|
+
def get_statuses(self) -> Dict[str, str]:
|
|
123
|
+
"""
|
|
124
|
+
Query and return the status of all relays.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict[str, str]: Dictionary mapping relay names (e.g., 'R1') to their status ('ON' or 'OFF').
|
|
128
|
+
Returns an empty dict if communication fails.
|
|
129
|
+
"""
|
|
130
|
+
status_cmd = bytes([0xFF, 0x09, 0x00])
|
|
131
|
+
try:
|
|
132
|
+
self.serial_utils.send(status_cmd)
|
|
133
|
+
response: Optional[List[int]] = self.serial_utils.receive(
|
|
134
|
+
self.RELAY_COUNT, as_int_list=True
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Failed to communicate with relay board: {e}")
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
if not response or len(response) < self.RELAY_COUNT:
|
|
141
|
+
logger.error("Failed to read relay status or incomplete response.")
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
relay_status = {
|
|
145
|
+
f"R{i}": self.STATUS_ON if byte == 1 else self.STATUS_OFF
|
|
146
|
+
for i, byte in enumerate(response, start=1)
|
|
147
|
+
}
|
|
148
|
+
logger.info("Relay statuses: %s", ", ".join(f"{k}: {v}" for k, v in relay_status.items()))
|
|
149
|
+
return relay_status
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> "RelayController":
|
|
152
|
+
"""
|
|
153
|
+
Enter the runtime context related to this object.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
RelayController: The connected relay controller instance.
|
|
157
|
+
"""
|
|
158
|
+
self.connect()
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def __exit__(
|
|
162
|
+
self,
|
|
163
|
+
exc_type: Optional[type],
|
|
164
|
+
exc_val: Optional[BaseException],
|
|
165
|
+
exc_tb: Optional[object],
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Exit the runtime context and close the serial connection.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
exc_type: Exception type.
|
|
172
|
+
exc_val: Exception value.
|
|
173
|
+
exc_tb: Exception traceback.
|
|
174
|
+
"""
|
|
175
|
+
self.close()
|
|
176
|
+
|
|
177
|
+
# --- Internal helpers ---
|
|
178
|
+
|
|
179
|
+
def _send_relay_command(self, relay_number: int, turn_on: bool) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Send a command to turn a relay ON or OFF.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
relay_number (int): Relay number (1-4).
|
|
185
|
+
turn_on (bool): True to turn ON, False to turn OFF.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
Exception: If sending the command fails.
|
|
189
|
+
"""
|
|
190
|
+
cmd = bytes([0xFF, relay_number, 0x01 if turn_on else 0x00])
|
|
191
|
+
logger.debug(
|
|
192
|
+
"Sending command: %s to relay %d (%s)",
|
|
193
|
+
cmd.hex(), relay_number, "ON" if turn_on else "OFF"
|
|
194
|
+
)
|
|
195
|
+
try:
|
|
196
|
+
self.serial_utils.send(cmd)
|
|
197
|
+
time.sleep(self.switch_delay)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Failed to send command to relay {relay_number}: {e}")
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
def _validate_relay_number(self, relay_number: int) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Validate that the relay number is within the allowed range.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
relay_number (int): Relay number to validate.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ValueError: If relay_number is not between 1 and RELAY_COUNT.
|
|
211
|
+
"""
|
|
212
|
+
if not (1 <= relay_number <= self.RELAY_COUNT):
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"relay_number must be between 1 and {self.RELAY_COUNT}, got {relay_number}."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
logging.basicConfig(level=logging.INFO)
|
|
219
|
+
print("Hello Kmtronic USB Relay User!")
|
|
220
|
+
|
|
221
|
+
# Example: Using RelayController directly
|
|
222
|
+
print("\n--- Example: RelayController ---")
|
|
223
|
+
try:
|
|
224
|
+
with RelayController("COM4") as relay:
|
|
225
|
+
relay.turn_on(1)
|
|
226
|
+
relay.turn_off(2)
|
|
227
|
+
print(f"Relay statuses: {relay.statuses}")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
print(f"Error: {e}")
|