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.
@@ -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.")