-
Notifications
You must be signed in to change notification settings - Fork 22
Added DC Load driver for Rigol DL3021 #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
daniel-montanari
merged 7 commits into
PyFixate:main
from
ckukas-ampcontrol:DCLoad_Driver
Jun 16, 2026
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
78158ba
Added DC Load driver for Rigol DL3021
ckukas-ampcontrol a785b7f
Ran pre-commit and updated formatting
ckukas-ampcontrol 7b82e36
Made changes based on D.M review. Changed 'Mode' from a class to a li…
ckukas-ampcontrol 4c55d33
Made changes based on D.M review. Changed 'Mode' from a class to a li…
ckukas-ampcontrol 7bae257
Minor updates based on D.M. comments in review. Ran tox -e mypy and r…
ckukas-ampcontrol 7357cb7
Changed set_current_range function so it accepts 'low', 'high' or 'de…
ckukas-ampcontrol 9fd3834
Merge remote-tracking branch 'upstream/main' into DCLoad_Driver
ckukas-ampcontrol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,4 +32,4 @@ | |
|
|
||
| from fixate.main import run_main_program as run | ||
|
|
||
| __version__ = "0.6.4" | ||
| __version__ = "0.6.5" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| """ | ||
| DC ELectronic Load driver | ||
| ========================= | ||
|
|
||
| Use `DCLoad.open()` to connect to a DC electronic load. | ||
| Functions are dictated by the abstract superclass ``DCLoad`` in helper.py | ||
| """ | ||
|
|
||
| import pyvisa | ||
|
|
||
| import fixate.drivers | ||
| from fixate.config import find_instrument_by_id | ||
| from fixate.drivers import InstrumentNotFoundError, InstrumentOpenError | ||
| from fixate.drivers.dcload.rigol_dl3021 import RigolDL3021 | ||
| from fixate.drivers.dcload.helper import DCLoad | ||
|
|
||
|
|
||
| def open() -> DCLoad: | ||
| """ | ||
| Connect to a DC electronic load. | ||
|
|
||
| Searches for a configured instrument and returns the first one found. | ||
|
|
||
| Returns: | ||
| DCLoad: open connection to the DCLoad | ||
| """ | ||
| for DCLoad in (RigolDL3021,): | ||
| instrument = find_instrument_by_id(DCLoad.REGEX_ID) | ||
| if instrument is not None: | ||
| # We've found a configured instrument so try to open it | ||
| rm = pyvisa.ResourceManager() | ||
| try: | ||
| resource = rm.open_resource(instrument.address) | ||
| except pyvisa.VisaIOError as e: | ||
| raise InstrumentOpenError( | ||
| f"Unable to open DCLoad: {instrument.address}" | ||
| ) from e | ||
| # Instantiate driver with connected instrument | ||
| driver = DCLoad(resource) | ||
| fixate.drivers.log_instrument_open(driver) | ||
| return driver | ||
| raise InstrumentNotFoundError |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| from typing import Protocol, Literal | ||
| from fixate.drivers import DriverProtocol | ||
|
|
||
| # List of modes that an electronic load can be set to | ||
| Mode = Literal[ | ||
| "constant_current", | ||
| "constant_voltage", | ||
| "constant_resistance", | ||
| "constant_power", | ||
| ] | ||
|
|
||
| CurrentRange = Literal[ | ||
| "low", | ||
| "high", | ||
| "default", | ||
| ] | ||
|
|
||
|
|
||
| class DCLoad(DriverProtocol, Protocol): | ||
| """Abstract class for DC electronic load drivers.""" | ||
|
|
||
| REGEX_ID: str | ||
|
|
||
| def __init__(self, instrument) -> None: | ||
| ... | ||
|
|
||
| def reset(self) -> None: | ||
| """Reset the instrument to a known state.""" | ||
| ... | ||
|
|
||
| def get_identity(self) -> str: | ||
| """Return instrument identity string.""" | ||
| ... | ||
|
|
||
| def set_mode(self, mode: Mode) -> None: | ||
| """Set the mode of the load (for example, constant current, constant voltage, etc.).""" | ||
| ... | ||
|
|
||
| def set_enabled(self, enable: bool) -> None: | ||
| """Enable (TRUE) or disable (FALSE) the load.""" | ||
| ... | ||
|
|
||
| def set_current(self, current: float) -> None: | ||
| """Set the load current to the specified value in Amps.""" | ||
| ... | ||
|
|
||
| def set_current_range(self, current_range: CurrentRange) -> None: | ||
| """Set the current range to the specified value in Amps.""" | ||
| ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| from typing import Any | ||
| from fixate.core.exceptions import InstrumentError, ParameterError | ||
| from fixate.drivers.dcload.helper import DCLoad, Mode, CurrentRange | ||
|
|
||
|
|
||
| class RigolDL3021(DCLoad): | ||
| """Driver for Rigol DL3021 DC electronic load.""" | ||
|
|
||
| REGEX_ID = "DL3021" | ||
| INSTR_TYPE = "VISA" | ||
|
|
||
| def __init__(self, instrument: Any) -> None: | ||
| self.instrument = instrument | ||
| self.instrument.timeout = 1000 | ||
| self._set_current_range: float | None = None | ||
|
|
||
| def _write(self, command: str) -> None: | ||
| """Write a command to the instrument.""" | ||
| try: | ||
| self.instrument.write(command) | ||
| except Exception as e: | ||
| raise InstrumentError(f"Error writing to instrument: {e}") from e | ||
|
|
||
| def _query(self, command: str) -> str: | ||
| """Query the instrument and return the response.""" | ||
| try: | ||
| return self.instrument.query(command) | ||
| except Exception as e: | ||
| raise InstrumentError(f"Error querying instrument: {e}") from e | ||
|
|
||
| def reset(self) -> None: | ||
| """Reset the instrument to default factory settings.""" | ||
| self._write("*RST") | ||
|
|
||
| def get_identity(self) -> str: | ||
| """Finds the instrument's manufacturer, model number, serial number, and firmware version.""" | ||
| return self._query("*IDN?").strip() | ||
|
|
||
| def set_mode(self, mode: Mode) -> None: | ||
| """Set the mode of the load (for example, constant current, constant voltage, etc.).""" | ||
| if mode == "constant_current": | ||
| scpi_mode = "CURR" | ||
| elif mode == "constant_voltage": | ||
| scpi_mode = "VOLT" | ||
| elif mode == "constant_resistance": | ||
| scpi_mode = "RES" | ||
| elif mode == "constant_power": | ||
| scpi_mode = "POW" | ||
| else: | ||
| raise ParameterError(f"Invalid mode: {mode}") | ||
|
|
||
| return self._write(f":SOUR:FUNC {scpi_mode}") | ||
|
|
||
| def _get_mode(self) -> str: | ||
| """Get the mode of the load.""" | ||
| return self._query(":SOUR:FUNC?").strip() | ||
|
|
||
| def set_enabled(self, enable: bool) -> None: | ||
| """Enable (TRUE) or disable (FALSE) the load.""" | ||
| value = "ON" if enable else "OFF" | ||
| return self._write(f":SOUR:INP:STAT {value}") | ||
|
|
||
| def _get_enabled(self) -> bool: | ||
| """Get the enabled state of the load.""" | ||
| state = self._query(":SOUR:INP:STAT?").strip() | ||
| return state == "1" | ||
|
|
||
| def _get_current(self) -> float: | ||
| """Get the current value in Amps.""" | ||
| return float(self._query(":SOUR:CURR:LEV:IMM?").strip()) | ||
|
|
||
| def set_current_range(self, current_range: CurrentRange) -> None: | ||
| """Set the current range to low (4A) or high (40A) or default (40A).""" | ||
| if current_range == "low": | ||
| scpi_mode = "MIN" | ||
| self._set_current_range = 4.0 | ||
| elif current_range == "high": | ||
| scpi_mode = "MAX" | ||
| self._set_current_range = 40.0 | ||
| elif current_range == "default": | ||
| self._set_current_range = 40.0 | ||
| scpi_mode = "DEF" | ||
| else: | ||
| raise ParameterError(f"Invalid current range: {current_range}") | ||
|
|
||
| return self._write(f":SOUR:CURR:RANG {scpi_mode}") | ||
|
|
||
| def _get_current_range(self) -> float: | ||
| """Get the current range in Amps. 4 or 40 for the DL3021.""" | ||
| return float(self._query(":SOUR:CURR:RANG?").strip()) | ||
|
|
||
| def set_current(self, current: float) -> None: | ||
| """Set the current to the specified value in Amps.""" | ||
| if self._set_current_range is None: | ||
| raise ParameterError("Current range must be set before setting current.") | ||
|
|
||
| if current > self._set_current_range: | ||
| raise ParameterError( | ||
| f"Current {current}A exceeds set current range of {self._set_current_range}A" | ||
| ) | ||
|
|
||
| return self._write(f":SOUR:CURR:LEV:IMM {current}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import time | ||
| from typing import Literal | ||
| import pytest | ||
| from fixate.config import load_config | ||
| import fixate.drivers.dcload | ||
| from fixate.drivers.dcload.rigol_dl3021 import RigolDL3021 | ||
|
|
||
| load_config() # Load fixate config file | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| def test_open_dcload(): | ||
| """Test that we can open a connection to the DC load.""" | ||
| opened = fixate.drivers.dcload.open() | ||
| assert opened, "Could not open DCLoad" | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| def test_get_identity(dcload: RigolDL3021): | ||
| """Test that we can get the identity string from the DC load.""" | ||
| iden = dcload.get_identity() | ||
| assert "DL3021" in iden | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| def test_enable_load(dcload: RigolDL3021): | ||
| """Test that we can enable and disable the load.""" | ||
| # Turn load ON | ||
| dcload.set_enabled(True) | ||
| assert dcload._get_enabled() is True, "Load should be ON" | ||
| # Turn load OFF | ||
| dcload.set_enabled(False) | ||
| assert dcload._get_enabled() is False, "Load should be OFF" | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| @pytest.mark.parametrize( | ||
| "mode, expected", | ||
| [ | ||
| ("constant_current", "CC"), | ||
| ("constant_voltage", "CV"), | ||
| ("constant_resistance", "CR"), | ||
| ("constant_power", "CP"), | ||
| ], | ||
| ) | ||
| def test_set_mode( | ||
| dcload: RigolDL3021, | ||
| mode: Literal["constant_current"] | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| | Literal["constant_voltage"] | ||
| | Literal["constant_resistance"] | ||
| | Literal["constant_power"], | ||
| expected: Literal["CC"] | Literal["CV"] | Literal["CR"] | Literal["CP"], | ||
| ): | ||
| """Verify that set_mode correctly sets the instrument mode.""" | ||
| dcload.set_mode(mode) | ||
| actual = dcload._get_mode() | ||
|
|
||
| assert actual == expected, f"Expected {expected}, got {actual}" | ||
|
|
||
| dcload.set_enabled(False) | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| @pytest.mark.parametrize( | ||
| "min_current, max_current, num_steps, duration", | ||
| [ | ||
| (0.0, 1.0, 6, 2), | ||
| ], | ||
| ) | ||
| def test_set_current( | ||
| dcload: RigolDL3021, | ||
| min_current: float, | ||
| max_current: float, | ||
| num_steps: Literal[6], | ||
| duration: Literal[2], | ||
| ): | ||
| """Test setting the current on the DC load.""" | ||
| # Generate evenly spaced current values (inclusive) | ||
| if num_steps == 1: | ||
| currents = [min_current] | ||
| else: | ||
| step = (max_current - min_current) / (num_steps - 1) | ||
| currents = [min_current + i * step for i in range(num_steps)] | ||
|
|
||
| # Set mode and range | ||
| dcload.set_mode("constant_current") | ||
| dcload.set_current_range("high") | ||
| dcload.set_enabled(True) | ||
|
|
||
| for current in currents: | ||
| dcload.set_current(current) | ||
| time.sleep(duration) | ||
| setpoint = float(dcload._get_current()) | ||
|
|
||
| assert abs(setpoint - current) < 1e-3, f"Expected {current}A, got {setpoint}A" | ||
|
|
||
| dcload.set_enabled(False) | ||
|
|
||
|
|
||
| @pytest.mark.drivertest | ||
| def test_reset(dcload: RigolDL3021): | ||
| """Test that resetting the instrument clears errors.""" | ||
| dcload.reset() | ||
| query = dcload._query("SYST:ERR?") | ||
| assert '0,"No error"' == query.strip() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.