rs_pro_power_supply 0.26.0__tar.gz
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.
- rs_pro_power_supply-0.26.0/.gitignore +65 -0
- rs_pro_power_supply-0.26.0/PKG-INFO +75 -0
- rs_pro_power_supply-0.26.0/README.md +57 -0
- rs_pro_power_supply-0.26.0/pyproject.toml +83 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/__init__.py +147 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/rsd3305p.py +1161 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/rsd3305p.yaml +43 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/rsd3305p_cs.py +320 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/rsd3305p_devif.py +216 -0
- rs_pro_power_supply-0.26.0/src/egse/power_supply/rs_pro/rsd3305p_protocol.py +129 -0
- rs_pro_power_supply-0.26.0/src/rs_pro_power_supply/__init__.py +0 -0
- rs_pro_power_supply-0.26.0/src/rs_pro_power_supply/cgse_explore.py +19 -0
- rs_pro_power_supply-0.26.0/src/rs_pro_power_supply/cgse_services.py +88 -0
- rs_pro_power_supply-0.26.0/src/rs_pro_power_supply/settings.yaml +21 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python versions and environment
|
|
2
|
+
|
|
3
|
+
__pycache__
|
|
4
|
+
.python-version
|
|
5
|
+
.envrc
|
|
6
|
+
|
|
7
|
+
# Build systems
|
|
8
|
+
|
|
9
|
+
build
|
|
10
|
+
dist
|
|
11
|
+
**/*.egg-info
|
|
12
|
+
|
|
13
|
+
# Apple specific
|
|
14
|
+
|
|
15
|
+
.DS_Store
|
|
16
|
+
|
|
17
|
+
# Unit testing
|
|
18
|
+
|
|
19
|
+
.pytest_cache
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
.nox
|
|
23
|
+
htmlcov
|
|
24
|
+
libs/cgse-core/temperature.csv
|
|
25
|
+
|
|
26
|
+
# Prototypes
|
|
27
|
+
|
|
28
|
+
**/prototypes/typed_serialization
|
|
29
|
+
|
|
30
|
+
# dotenv, ignore all but the example file
|
|
31
|
+
|
|
32
|
+
.env
|
|
33
|
+
.env.*
|
|
34
|
+
!.env.example
|
|
35
|
+
|
|
36
|
+
# Virtual environments
|
|
37
|
+
|
|
38
|
+
.venv
|
|
39
|
+
venv
|
|
40
|
+
|
|
41
|
+
# PyCharm IDE
|
|
42
|
+
|
|
43
|
+
.idea
|
|
44
|
+
|
|
45
|
+
# VSCode IDE
|
|
46
|
+
|
|
47
|
+
.vscode
|
|
48
|
+
*.code-workspace
|
|
49
|
+
|
|
50
|
+
# MKDOCS documentation site
|
|
51
|
+
|
|
52
|
+
/site
|
|
53
|
+
|
|
54
|
+
# Packaging
|
|
55
|
+
|
|
56
|
+
uv.lock
|
|
57
|
+
libs/test_service_registry.db
|
|
58
|
+
libs/cgse-core/metrics.duckdb
|
|
59
|
+
install_influxdb3.sh
|
|
60
|
+
.pylintrc
|
|
61
|
+
|
|
62
|
+
# CGSE Specific
|
|
63
|
+
|
|
64
|
+
.migrate_influx_to_questdb.state.json
|
|
65
|
+
service_registry.db*
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rs_pro_power_supply
|
|
3
|
+
Version: 0.26.0
|
|
4
|
+
Summary: RS-PRO RS-D3305P Power Supply
|
|
5
|
+
Author: IvS KU Leuven
|
|
6
|
+
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: CGSE,Common-EGSE,hardware testing,software framework
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: cgse-common
|
|
11
|
+
Requires-Dist: cgse-core
|
|
12
|
+
Requires-Dist: pyserial>=3.5
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == 'test'
|
|
15
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
16
|
+
Requires-Dist: pytest-mock; extra == 'test'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# RS-PRO RS-D3305P Power Supply
|
|
20
|
+
|
|
21
|
+
This package provides a Python interface to the RS-PRO RS-D3305P power supply. It allows for commanding and monitoring of the power supply via a serial connection.
|
|
22
|
+
|
|
23
|
+
## Local Settings
|
|
24
|
+
|
|
25
|
+
This is what you need to add to your local settings file:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
RS-PRO RS-D3305P Controller:
|
|
29
|
+
<arbitrary name to refer to the device>:
|
|
30
|
+
SERIAL_NUMBER: <serial number of the device, between quotes, e.g. "123456789">
|
|
31
|
+
|
|
32
|
+
RS-PRO RS-D3305P Control Server:
|
|
33
|
+
PROTOCOL: tcp
|
|
34
|
+
<same arbitrary name to refer to the device>:
|
|
35
|
+
SERVICE_TYPE: <same arbitrary name to refer to the device>
|
|
36
|
+
PROCESS_NAME: <same arbitrary name to refer to the device>
|
|
37
|
+
HOST: <hostname on which the CS is running, e.g. "localhost">
|
|
38
|
+
STORAGE_MNEMONIC: <same arbitrary name to refer to the device>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
To get hold of the serial number of the device, you can use the following command:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from egse.power_supply.rs_pro.rsd3305p_devif import print_serial_ports
|
|
45
|
+
|
|
46
|
+
print_serial_ports()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For each serial port on the computer, the port name and corresponding serial number of the device (if any) will be
|
|
50
|
+
printed. Select the serial number of the device you want to connect to and add it to your local settings file, as indicated above.
|
|
51
|
+
|
|
52
|
+
In case the RS-D3305P device does not show up in the list of serial ports, while the USB cable is connected, try power-cycling the device and try again.
|
|
53
|
+
|
|
54
|
+
# Control Server
|
|
55
|
+
|
|
56
|
+
To start the Control Server for the RS-D3305P device, use the following command:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
$ cgse rsd3305 start [--sim] <same arbitrary name to refer to the device>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The optional `--sim` flag can be used to start the Control Server in simulation mode, which does not require a physical device to be connected.
|
|
63
|
+
|
|
64
|
+
## Limitations
|
|
65
|
+
|
|
66
|
+
Initially, the idea was to connect to the RS-3305P power supply via an Ethernet cable and do the commanding via TCP/IP. However, this only works for write commands and not for read commands. Therefore, we use a serial connection instead.
|
|
67
|
+
|
|
68
|
+
The RS-3305P power supply has a USB port that can be used to connect to a computer via a USB cable. The USB connection is recognised as a virtual COM port, which allows for serial communication with the power supply.
|
|
69
|
+
|
|
70
|
+
## Reference Documents
|
|
71
|
+
|
|
72
|
+
- RS-D3305P IP LAN Communication Protocol
|
|
73
|
+
- RS-D3305P Series Multiple Channel Remote Control Syntax v4.0
|
|
74
|
+
- RS-D3305P DC Power Supply Series User Manual
|
|
75
|
+
- [RS Pro D3305P - Python GUI Remote Controller](https://github.com/LucarVicti/rs-d3305p-gui-controller)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# RS-PRO RS-D3305P Power Supply
|
|
2
|
+
|
|
3
|
+
This package provides a Python interface to the RS-PRO RS-D3305P power supply. It allows for commanding and monitoring of the power supply via a serial connection.
|
|
4
|
+
|
|
5
|
+
## Local Settings
|
|
6
|
+
|
|
7
|
+
This is what you need to add to your local settings file:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
RS-PRO RS-D3305P Controller:
|
|
11
|
+
<arbitrary name to refer to the device>:
|
|
12
|
+
SERIAL_NUMBER: <serial number of the device, between quotes, e.g. "123456789">
|
|
13
|
+
|
|
14
|
+
RS-PRO RS-D3305P Control Server:
|
|
15
|
+
PROTOCOL: tcp
|
|
16
|
+
<same arbitrary name to refer to the device>:
|
|
17
|
+
SERVICE_TYPE: <same arbitrary name to refer to the device>
|
|
18
|
+
PROCESS_NAME: <same arbitrary name to refer to the device>
|
|
19
|
+
HOST: <hostname on which the CS is running, e.g. "localhost">
|
|
20
|
+
STORAGE_MNEMONIC: <same arbitrary name to refer to the device>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To get hold of the serial number of the device, you can use the following command:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from egse.power_supply.rs_pro.rsd3305p_devif import print_serial_ports
|
|
27
|
+
|
|
28
|
+
print_serial_ports()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For each serial port on the computer, the port name and corresponding serial number of the device (if any) will be
|
|
32
|
+
printed. Select the serial number of the device you want to connect to and add it to your local settings file, as indicated above.
|
|
33
|
+
|
|
34
|
+
In case the RS-D3305P device does not show up in the list of serial ports, while the USB cable is connected, try power-cycling the device and try again.
|
|
35
|
+
|
|
36
|
+
# Control Server
|
|
37
|
+
|
|
38
|
+
To start the Control Server for the RS-D3305P device, use the following command:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
$ cgse rsd3305 start [--sim] <same arbitrary name to refer to the device>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The optional `--sim` flag can be used to start the Control Server in simulation mode, which does not require a physical device to be connected.
|
|
45
|
+
|
|
46
|
+
## Limitations
|
|
47
|
+
|
|
48
|
+
Initially, the idea was to connect to the RS-3305P power supply via an Ethernet cable and do the commanding via TCP/IP. However, this only works for write commands and not for read commands. Therefore, we use a serial connection instead.
|
|
49
|
+
|
|
50
|
+
The RS-3305P power supply has a USB port that can be used to connect to a computer via a USB cable. The USB connection is recognised as a virtual COM port, which allows for serial communication with the power supply.
|
|
51
|
+
|
|
52
|
+
## Reference Documents
|
|
53
|
+
|
|
54
|
+
- RS-D3305P IP LAN Communication Protocol
|
|
55
|
+
- RS-D3305P Series Multiple Channel Remote Control Syntax v4.0
|
|
56
|
+
- RS-D3305P DC Power Supply Series User Manual
|
|
57
|
+
- [RS Pro D3305P - Python GUI Remote Controller](https://github.com/LucarVicti/rs-d3305p-gui-controller)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rs_pro_power_supply"
|
|
3
|
+
version = "0.26.0"
|
|
4
|
+
description = "RS-PRO RS-D3305P Power Supply"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "IvS KU Leuven"}
|
|
7
|
+
]
|
|
8
|
+
maintainers = [
|
|
9
|
+
{name = "Rik Huygen", email = "rik.huygen@kuleuven.be"},
|
|
10
|
+
{name = "Sara Regibo", email = "sara.regibo@kuleuven.be"}
|
|
11
|
+
]
|
|
12
|
+
readme = {"file" = "README.md", "content-type" = "text/markdown"}
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
keywords = [
|
|
16
|
+
"CGSE",
|
|
17
|
+
"Common-EGSE",
|
|
18
|
+
"hardware testing",
|
|
19
|
+
"software framework",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"cgse-common",
|
|
23
|
+
"cgse-core",
|
|
24
|
+
"pyserial>=3.5",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = ["pytest", "pytest-mock", "pytest-cov"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
rsd3305p_cs = 'egse.power_supply.rs_pro.rsd3305p_cs:app'
|
|
32
|
+
#rsd3305p_sim = 'egse.power_supply.rs_pro.rsd3305p_sim:app'
|
|
33
|
+
|
|
34
|
+
#[project.gui-scripts]
|
|
35
|
+
#rsd3305p_ui = "egse.power_supply.rs_pro.rsd3305p_ui:main"
|
|
36
|
+
|
|
37
|
+
[project.entry-points."cgse.version"]
|
|
38
|
+
rs-pro-power-supply = 'egse.version:get_version_installed'
|
|
39
|
+
|
|
40
|
+
[project.entry-points."cgse.settings"]
|
|
41
|
+
rs-pro-power-supply = "rs_pro_power_supply:settings.yaml"
|
|
42
|
+
|
|
43
|
+
[project.entry-points."cgse.service.device_command"]
|
|
44
|
+
rsd3305p = 'rs_pro_power_supply.cgse_services:rsd3305p'
|
|
45
|
+
|
|
46
|
+
[project.entry-points."cgse.explore"]
|
|
47
|
+
explore = "rs_pro_power_supply.cgse_explore"
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
exclude = [
|
|
54
|
+
"/tests",
|
|
55
|
+
"/pytest.ini",
|
|
56
|
+
"/.gitignore",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.wheel]
|
|
60
|
+
packages = ["src/egse", "src/rs_pro_power_supply"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 120
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
extend-select = ["E501"]
|
|
67
|
+
|
|
68
|
+
[build-system]
|
|
69
|
+
requires = ["hatchling"]
|
|
70
|
+
build-backend = "hatchling.build"
|
|
71
|
+
|
|
72
|
+
[dependency-groups]
|
|
73
|
+
dev = [
|
|
74
|
+
"setuptools", # needed by PyCharm
|
|
75
|
+
"pytest>=8.3.4",
|
|
76
|
+
"pytest-cov>=6.0.0",
|
|
77
|
+
"pytest-mock>=3.14.0",
|
|
78
|
+
"ruff>=0.9.0",
|
|
79
|
+
"nox>=2025.2.9",
|
|
80
|
+
"setuptools>=75.8.2", # needed by PyCharm
|
|
81
|
+
"pytest-asyncio>=0.26.0",
|
|
82
|
+
"ipython",
|
|
83
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from egse.settings import Settings
|
|
5
|
+
|
|
6
|
+
HERE = Path(__file__).parent
|
|
7
|
+
|
|
8
|
+
DEVICE_SETTINGS = Settings.load("RS-PRO RS-D3305P Controller")
|
|
9
|
+
CS_SETTINGS = Settings.load("RS-PRO RS-D3305P Control Server")
|
|
10
|
+
PROTOCOL = CS_SETTINGS.get("PROTOCOL", "tcp") # Communication protocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROXY_TIMEOUT = 10
|
|
14
|
+
|
|
15
|
+
ENCODING = "ascii"
|
|
16
|
+
LINE_ENDING = "\n"
|
|
17
|
+
IDENTIFICATION_QUERY = "*IDN?"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OperatingMode(IntEnum):
|
|
21
|
+
"""Enumeration of the operating modes supported by the RS-PRO RS-D3305P power supply."""
|
|
22
|
+
|
|
23
|
+
INDEPENDENT = 0
|
|
24
|
+
TRACKING_SERIES = 1
|
|
25
|
+
TRACKING_PARALLEL = 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def split_result_on_blanks(response: bytes) -> tuple[str, ...]:
|
|
29
|
+
"""Splits the given response string on blanks.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
response (str): Response to split.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of strings split on commas.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
return tuple(to_string(response).split(" "))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to_string(response: bytes) -> str:
|
|
42
|
+
"""Converts the given response to a string.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
response (bytes): Response to convert.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
String value converted from the given response.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
return response.decode(ENCODING, errors="ignore").strip("\r\n\x00 ")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def to_float(response: bytes) -> float:
|
|
55
|
+
"""Converts the given response string to a float.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
response (bytes): Response to convert.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Float value converted from the given response.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
return float(to_string(response))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_int(response: bytes) -> int:
|
|
68
|
+
"""Converts the given response string to an integer value.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
response (bytes): Response to convert.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Integer value converted from the given response.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return int(to_string(response))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def decode_status(response: bytes) -> dict:
|
|
81
|
+
"""Decodes the status byte from the given response.
|
|
82
|
+
|
|
83
|
+
The response an 8-bit number in the following format:
|
|
84
|
+
|
|
85
|
+
- bit 0: 0 when channel 1 is operated in CC mode, 1 when channel 1 is operated in CV mode;
|
|
86
|
+
- bit 1: 0 when channel 2 is operated in CC mode, 1 when channel 2 is operated in CV mode;
|
|
87
|
+
- bits 2 and 3: operating mode (00: independent, 01: tracking series, 10: tracking parallel);
|
|
88
|
+
- bits 4 and 5 -> not mentioned in the documentation;
|
|
89
|
+
- bit 6: 0 if the output of channel 1 is off, 1 if the output of channel 1 is on;
|
|
90
|
+
- bit 7: 0 if the output of channel 2 is off, 1 if the output of channel 2 is on.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
response (bytes): Response to decode (status byte).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary of decoded status bytes. The keys in this dictionary are:
|
|
97
|
+
|
|
98
|
+
- "ch1_mode": "CC" or "CV" (mode of channel 1);
|
|
99
|
+
- "ch2_mode": "CC" or "CV" (mode of channel 2);
|
|
100
|
+
- "tracking": "Independent", "Series", or "Parallel" (operating mode);
|
|
101
|
+
- "ch1_on": True if the output of channel 1 is on, False otherwise;
|
|
102
|
+
- "ch2_on": True if the output of channel 2 is on, False otherwise.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
status_byte = response[0]
|
|
106
|
+
|
|
107
|
+
# - bit 0: 0 when channel 1 is operated in CC mode, 1 when channel 1 is operated in CV mode;
|
|
108
|
+
# - bit 1: 0 when channel 2 is operated in CC mode, 1 when channel 2 is operated in CV mode;
|
|
109
|
+
# - bits 2 and 3: operating mode (00: independent, 01: tracking series, 10: tracking parallel);
|
|
110
|
+
# - bits 4 and 5 -> not mentioned in the documentation;
|
|
111
|
+
# - bit 6: 0 if the output of channel 1 is off, 1 if the output of channel 1 is on;
|
|
112
|
+
# - bit 7: 0 if the output of channel 2 is off, 1 if the output of channel 2 is on.
|
|
113
|
+
|
|
114
|
+
ch1_mode = "CV" if (status_byte & 0b00000001) else "CC" # Bit 0
|
|
115
|
+
ch2_mode = "CV" if (status_byte & 0b00000010) else "CC" # Bit 1
|
|
116
|
+
|
|
117
|
+
tracking_code = (status_byte >> 2) & 0b11 # Bits 2 & 3
|
|
118
|
+
tracking = {0: "Independent", 1: "Series", 2: "Parallel"}.get(tracking_code, f"Unknown({tracking_code})")
|
|
119
|
+
|
|
120
|
+
# Bits 4 & 5: Not mentioned in the documentation
|
|
121
|
+
|
|
122
|
+
ch1_on = True if (status_byte & 0b01000000) else False # Bit 6
|
|
123
|
+
ch2_on = True if (status_byte & 0b0100000) else False # Bit 7
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"ch1_mode": ch1_mode,
|
|
127
|
+
"ch2_mode": ch2_mode,
|
|
128
|
+
"tracking": tracking,
|
|
129
|
+
"ch1_on": ch1_on,
|
|
130
|
+
"ch2_on": ch2_on,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def check_channel(kwargs: dict):
|
|
135
|
+
"""Checks whether the given channel is 1 or 2.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
kwargs (dict): Dictionary of arguments passed to the function.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError if the channel is neither 1 nor 2.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
channel = kwargs.get("channel")
|
|
145
|
+
|
|
146
|
+
if channel not in (1, 2):
|
|
147
|
+
raise ValueError("Channel must be 1 or 2.")
|