diff --git a/src/Switch-Arduino_MCP4728/main.py b/src/Switch-Arduino_MCP4728/main.py index 76789560..a39ec015 100644 --- a/src/Switch-Arduino_MCP4728/main.py +++ b/src/Switch-Arduino_MCP4728/main.py @@ -25,28 +25,53 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +# SweepMe! driver +# *Module: Switch +# *Instrument: Arduino MCP4728 -# SweepMe! device class -# Device: Arduino MCP4728 - +from __future__ import annotations from pysweepme import EmptyDevice class Device(EmptyDevice): + """Base class to control MCP4728 boards through Arduino.""" + description = """
This driver allows to set output voltages at MCP 4728 boards with 12-bit resolution. It can control up to 8 boards, each with 4 pins.
++ This driver allows to set output voltages at MCP 4728 boards with 12-bit resolution. It can control up to 8 + boards, each with 4 pins. +
Load the Switch-Arduino_MCP.ino sketch onto your Arduino. Set baudrate to 115200 and terminator to "\n". Install the Adafruit_MCP4728 library on your Arduino.
++ Load the Switch-Arduino_MCP.ino sketch onto your Arduino. Set baudrate to 115200 and + terminator to "\n". Install the Adafruit_MCP4728 library on your Arduino. +
Set Channel to the pin number you want to set, or all to set all four channels. The voltage values must be passed as a colon-separated string: 1.0:2.5:0:4.2
-The I²C address is set as integer 0-7, corresponding to the boards standard addresses 0x60-0x67 (HEX). You can check your devices' address by using an I²C Scanner.
-The maximum voltage is defined by the Voltage reference, which can either be internal (2.048 V or 4.096 V by using 2x gain) or from an external source, e.g. the Arduino's 5 V or 3.3 V output. When using an external reference, the voltage must be given.
-To use multiple MCPs, their I²C addresses need to be changed individually, as described here. Choose Channel: all and set a comma-separated list for I2C Address: 0, 1, 2.
++ Set Channel to the pin number you want to set, or all to set all four channels. The + voltage values must be passed as a colon-separated string: 1.0:2.5:0:4.2 +
++ The I²C address is set as integer 0-7, corresponding to the boards standard addresses 0x60-0x67 (HEX). You + can check your devices' address by using an + I²C Scanner. +
++ The maximum voltage is defined by the Voltage reference, which can either be internal (2.048 V or 4.096 V by + using 2x gain) or from an external source, e.g. the Arduino's 5 V or 3.3 V output. When using an external + reference, the voltage must be given. +
++ To use multiple MCPs, their I²C addresses need to be changed individually, as described + here. Choose Channel: all and + set a comma-separated list for I2C Address: 0, 1, 2. +
""" def __init__(self) -> None: + """Initialize the Device Class.""" EmptyDevice.__init__(self) self.shortname = "Arduino MCP4728" @@ -60,6 +85,16 @@ def __init__(self) -> None: "timeout": 5, "baudrate": 115200, } + self.port_str: str = "" + self.driver_name: str = "" + self.instance_key: str = "" + + self.pin: int = 0 + self.sweepmode: str = "" + self.reference_voltage: str = "Internal 4.096 V" + self.external_voltage: float = 5.0 + self.value_list: list[float] = [] + self.volt: float = 0 self.multi_pins = False self.multi_mcp = False @@ -71,7 +106,8 @@ def __init__(self) -> None: "Output in %": "%", } - def set_GUIparameter(self): + def set_GUIparameter(self) -> dict: # noqa: N802 + """Set standard GUI parameters for the SweepMe! software.""" return { "Channel": ["0", "1", "2", "3", "all"], "I2C Address": "0", @@ -80,7 +116,8 @@ def set_GUIparameter(self): "External voltage in V": 5.0, } - def get_GUIparameter(self, parameter={}): + def get_GUIparameter(self, parameter: dict) -> None: # noqa: N802 + """Handle input from GUI.""" # Handle output pins channel = parameter["Channel"] if channel == "all": @@ -116,10 +153,9 @@ def get_GUIparameter(self, parameter={}): self.variables = [] self.units = [] for n in range(channel_num): - # If single channel, use the correct pin number - if channel_num == 1: - n = self.pin - self.variables.append(f"Channel{n}") + # If single channel, use the correct pin + pin = self.pin if channel_num == 1 else n + self.variables.append(f"Channel{pin}") self.units.append(self.unit[self.sweepmode]) self.port_str = parameter["Port"] @@ -127,7 +163,8 @@ def get_GUIparameter(self, parameter={}): """ here, semantic standard functions start that are called by SweepMe! during a measurement """ - def connect(self): + def connect(self) -> None: + """Initialize Arduino and register the device in the communication manager.""" # Set Name/Number of COM Port as key self.instance_key = f"{self.driver_name}_{self.port_str}" @@ -136,11 +173,13 @@ def connect(self): self.port.read() self.device_communication[self.instance_key] = "Connected" - def disconnect(self): + def disconnect(self) -> None: + """Unregister the device from the communication manager.""" if self.instance_key in self.device_communication: self.device_communication.pop(self.instance_key) - def configure(self): + def configure(self) -> None: + """Configure the MCP4728 reference voltages.""" # Initialize single MCP4728 with given I2C address - multi MCPs are set in apply if not self.multi_mcp: self.set_address(self.addresses[0]) @@ -158,8 +197,8 @@ def configure(self): self.set_vref(use_internal_vref=False) self.max_voltage = self.external_voltage - def unconfigure(self): - # Set all outputs to 0 + def unconfigure(self) -> None: + """Set all outputs to 0 V after measurement finishes.""" if self.multi_pins: for address in self.addresses: self.set_address(address) @@ -168,17 +207,20 @@ def unconfigure(self): else: self.set_voltage(self.pin, 0) - def apply(self): + def apply(self) -> None: + """Set the voltages to the pins.""" if self.multi_pins: - # Receive values as list, split by ":" and convert to float - split_values = self.value.split(":") - self.value_list = list(map(float, split_values)) + # Replace , with . to enable float conversion + dot_separated_values = self.value.replace(",", ".") + # Input values are separated by colons + sweep_values = dot_separated_values.split(":") + self.value_list = list(map(float, sweep_values)) # Adjust number of channels depending on number of boards and pins number_of_channels = 4 * len(self.addresses) if self.multi_mcp else 4 if len(self.value_list) != number_of_channels: - msg = f"Incorrect number of voltages received. Expected {number_of_channels}, got {len(value_list)}" + msg = f"Incorrect number of voltages received. Expected {number_of_channels} got {len(self.value_list)}" raise Exception(msg) for n, value in enumerate(self.value_list): @@ -188,7 +230,7 @@ def apply(self): else: volt_bit = self.voltage_to_12bit(value) - # Iterate through pin numbers (0-3) and initialize MCP for each pin=0 + # Iterate through pins (0-3) and initialize MCP for each pin=0 pin = n % 4 if pin == 0: mcp_num = int(n / 4) @@ -211,24 +253,25 @@ def apply(self): self.set_voltage(self.pin, volt_bit) - def call(self): - # Return values that are set to the pins + def call(self) -> list[float]: + """Return the voltages set to the pins.""" return self.value_list if self.multi_pins else [self.volt] """ here, convenience functions start """ - def voltage_to_12bit(self, voltage, relative_voltage=False): - # Convert given voltage to integer between 0 and 4095 (max voltage) + def voltage_to_12bit(self, voltage: float, relative_voltage: bool = False) -> int: + """Convert given voltage to integer between 0 and 4095 (max voltage).""" voltage_bit = int(voltage / 100 * 4095) if relative_voltage else int(4095 / self.max_voltage * voltage) - if not 0 <= voltage_bit < 4096: + max_bit = 4095 + if not 0 <= voltage_bit < max_bit: msg = f"Voltage {voltage} out of bound. Either larger than max voltage or negative." raise ValueError(msg) return voltage_bit - def set_voltage(self, pin, bit_value): - # Send command to set voltage (in bit value) at given pin + def set_voltage(self, pin: int, bit_value: int) -> None: + """Send command to set voltage (in bit) at given pin.""" command_string = f"CH{pin}={bit_value}" self.port.write(command_string) @@ -243,9 +286,10 @@ def set_voltage(self, pin, bit_value): msg = f"Failed to set voltage of {bit_value} at pin {pin}. Arduino response: {ret}" raise Exception(msg) - def set_address(self, address: int): - # Initialize MCP at Arduino to receive further commands - if not 0 <= address <= 7: + def set_address(self, address: int) -> None: + """Initialize MCP at Arduino to receive further commands.""" + max_address = 7 + if not 0 <= address <= max_address: msg = "I2C Address must be 0-7" raise Exception(msg) @@ -267,8 +311,8 @@ def set_address(self, address: int): msg = f"Failed to set address to {address}. Arduino response: {ret}" raise Exception(msg) - def set_vref(self, use_internal_vref=True): - # Set reference voltage as internal or external + def set_vref(self, use_internal_vref: bool = True) -> None: + """Set reference voltage as internal or external.""" v_ref = "I" if use_internal_vref else "E" command_string = f"VR={v_ref}" @@ -280,8 +324,8 @@ def set_vref(self, use_internal_vref=True): msg = f"Failed to set reference voltage (vref). Arduino response: {ret}" raise Exception(msg) - def set_gain(self, gain): - # For internal reference voltage, choose 1x or 2x gain + def set_gain(self, gain: int) -> None: + """For internal reference voltage, choose 1x or 2x gain.""" if gain not in [1, 2]: msg = "gain can only be 1 or 2" raise ValueError(msg)