HP 4141B SMU driver (alpha)

Alongside the HP 4142B, I’ve also access to a few HP 4141B at work which might come in handy for playing along the recently acquired Keysight B2902B. The 4141B shares a lot of similarities with the 4142B in terms of communciation protocol, which is why I had a go at modifying the 4142B (from now on, the “42”) base driver for usage with the 4141B (the “41”).

A few commands had to be modified, like the CLear All command that must not have any channel number as parameter as it is always clearing all channel setups at the same time but won’t do so if a channel number is supplied afterwards.

In contrast to the 42, the 41 does not seem to be able to store the value of the measured voltage as well as the measured current in the buffer at the same time. Therefore, it seems not possible to measure voltage and current in the measure-procedure and collect the results from the buffer inside the call procedure afterwards. As I am not very familiar with Python, the only solution I found was to generate global variables in which the measurement results are stored inside the measure procedure and to recall them in the call procedure afterwards. In addition, these parameters have to be channel-specific as the consecutive readout of the channels would otherwise override the preceding measurement values.

I see no ways of creating attachments so I will insert the code in the next reply.

Christian

1 Like
# This Device Class is published under the terms of the MIT License.
# Required Third Party Libraries, which are included in the Device Class
# package for convenience purposes, may have a different license. You can
# find those in the corresponding folders or contact the maintainer.
#
# MIT License
# 
# Copyright (c) 2019 Axel Fischer (sweep-me.net)
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# SweepMe! device class
# Type: SMU
# Device:HP 4141B

import numpy as np
import time
from collections import OrderedDict

from EmptyDeviceClass import EmptyDevice

class Device(EmptyDevice):

    multichannel = [" CH1", " CH2", " CH3", " CH4"]

    def __init__(self):
    
        EmptyDevice.__init__(self)
        
        # remains here for compatibility with v1.5.3        
        self.multichannel = [" CH1", " CH2", " CH3", " CH4"]

        self.variables = ["Voltage", "Current"]
        self.units     = ["V", "A"]
        self.plottype  = [True, True] # True to plot data
        self.savetype  = [True, True] # True to save data
        
        self.port_manager = True
        self.port_types = ['GPIB']
        self.port_properties = {    
                                    "timeout": 3,
                                    # "delay": 0.1,
                                }
                            
        # self.port_identifications = ['...']
        
        
        self.current_ranges = OrderedDict([
                                                ("Auto",                  "0"),
                                                ("1 nA limited auto",     "1"),
                                                ("10 nA limited auto",    "2"),
                                                ("100 nA limited auto",   "3"),
                                                ("1 μA limited auto",     "4"),
                                                ("10 μA limited auto",    "5"),
                                                ("100 μA limited auto",   "6"),
                                                ("1 mA limited auto",     "7"),
                                                ("10 mA limited auto",    "8"),
                                                ("100 mA limited auto",   "9"),
                                            ])
                                            

        
    def set_GUIparameter(self):
        
        GUIparameter = {
                        "SweepMode": ["Voltage [V]", "Current [A]"],
                        "RouteOut": ["Rear"],
                        # "Speed": ["Fast", "Medium", "Slow"],
                        "Range": list(self.current_ranges.keys()),
                        "Compliance": 100e-6,
                        "Average": 1,
                        }
                        
        return GUIparameter
        
        
    def get_GUIparameter(self, parameter={}):
        
        self.device = parameter['Device']
        self.four_wire = parameter['4wire']
        self.route_out = parameter['RouteOut']
        self.source = parameter['SweepMode']
        self.port_string = parameter['Port']
        
        
        self.irange = self.current_ranges[parameter['Range']]
        
        self.protection = parameter['Compliance']
        self.speed = parameter['Speed']
        self.pulse = parameter['CheckPulse']   
        self.pulse_meas_time = parameter['PulseMeasTime']
        
        self.average = int(parameter['Average'])
        
        self.channel = self.device[-1]
        
        self.shortname = "HP4141B CH%s" % self.channel
        
        # voltage autoranging 
        self.vrange = "0"
              
        
    def initialize(self):
    
        unique_DC_port_string = "HP4141B_" + self.port_string

        # initialize commands only need to be sent once, so we check here whether another instance of the same Device Class AND same port did it already. If not, this instance is the first and has to do it.
        if not unique_DC_port_string in self.device_communication:
        
            self.port.write("ID") # identification
            print(self.port.read())
            #print(unique_DC_port_string)
            
            self.port.write("*RST?") # reset to initial settings
                       
            self.port.write("BC")     # buffer clear
            
            self.port.write("FMT 2") # use to change the output format
         
            # if initialize commands have been sent, we can add the the unique_DC_port_string to the dictionary that is seen by all Device Classes
            #self.device_communication[unique_DC_port_string] = True
          
    def configure(self):
    
        ### The command CN has to be sent at the beginning as it resets certain parameters
        ### If the command CN would be used later, e.g. durin  'poweron', it would overwrite parameters that are defined during 'configure'
        self.port.write("CN " + self.channel) # switches the channel on
    
        """
        if self.speed == "Fast": # 1 Short (0.1 PLC) preconfigured selection Fast
            self.port.write("AIT 0,0,1")
            self.port.write("AAD %s,0" % self.channel)
        elif self.speed == "Medium": # 2 Medium (1.0 PLC) preconfigured selection Normal
            self.port.write("AIT 1,0,1")
            self.port.write("AAD %s,1" % self.channel)
        elif self.speed == "Slow": # 3 Long (10 PLC) preconfigured selection Quiet
            self.port.write("AIT 1,2,10")
            self.port.write("AAD %s,1" % self.channel)
        """
        
    def unconfigure(self):
        self.port.write("IN" + self.channel)
        # resets to zero volt
        # self.port.write("DZ")
        
        """
        Enables channels CN [chnum ... [,chnum] ... ]
        Disables channels CL [chnum ... [,chnum] ... ]
        Sets filter ON/OFF [FL] mode[,chnum ... [,chnum] ... ]
        Sets series resistor ON/OFF [SSR] chnum,mode
        Sets integration time
        (Agilent B1500 can use
        AAD/AIT instead of AV.)
        [AV] number[,mode]
        [AAD] chnum[,type]
        [AIT] type,mode[,N]
        Forces constant voltage DV chnum,range,output
        [,comp[,polarity[,crange]]]
        Forces constant current DI
        Sets voltage measurement
        range
        [RV] chnum,range
        Sets current measurement
        range
        [RI] chnum,range
        [RM] chnum,mode[,rate]
        Sets measurement mode MM 1,chnum[,chnum ... [,chnum] ... ]
        Sets SMU operation mode [CMM] chnum,mode
        Executes measurement XE
        """
        

    def deinitialize(self):
        pass
    
    def poweron(self):
        pass
        # In a previous version, the CN command was sent here. However, this leads to a reset of all parameters previously changed during 'configure' 
        # Therefore, the CN command should not be used here, but has been moved to the beginning of 'configure'
    
    def poweroff(self):
        self.port.write("CL") # switches all channels off; do not supply channel numbers as arguments, will not work on HP4141B
      
    def apply(self):

        self.value = str(self.value)
        
        if self.source.startswith("Voltage"):
           self.port.write("DV " + self.channel + "," + self.vrange + "," + self.value + "," + self.protection)

        if self.source.startswith("Current"):
           self.port.write("DI " + self.channel + "," + self.irange + "," + self.value + "," + self.protection)

    # def trigger(self):
        # pass
        # software trigger to start the measurement
        # self.port.write("XE")
        
    def measure(self):
    
        if self.channel=="1":
            global v_CH1,i_CH1
            self.port.write("TI " + self.channel)
            answer_CH1 = self.port.read()
            i_CH1 = float(answer_CH1[3:]) #removes first three characters (can be "NAI", "NAV", "XAI", "XAV") and converts value to float
            self.port.write("TV " + self.channel)
            answer_CH1 = self.port.read()  
            v_CH1 = float(answer_CH1[3:])

        if self.channel=="2":
            global v_CH2,i_CH2
            self.port.write("TI " + self.channel)
            answer_CH2 = self.port.read()
            i_CH2 = float(answer_CH2[3:])
            self.port.write("TV " + self.channel)
            answer_CH2 = self.port.read()  
            v_CH2 = float(answer_CH2[3:])

        if self.channel=="3":
            global v_CH3,i_CH3
            self.port.write("TI " + self.channel)
            answer_CH3 = self.port.read()
            i_CH3 = float(answer_CH3[3:])
            self.port.write("TV " + self.channel)
            answer_CH3 = self.port.read()  
            v_CH3 = float(answer_CH3[3:])

        if self.channel=="4":
            global v_CH4,i_CH4
            self.port.write("TI " + self.channel)
            answer_CH4 = self.port.read()
            i_CH4 = float(answer_CH4[3:])
            self.port.write("TV " + self.channel)
            answer_CH4 = self.port.read()  
            v_CH4 = float(answer_CH4[3:])
       
    def call(self):
    


        if self.channel=="1":
            return [v_CH1,i_CH1]

        if self.channel=="2":
            return [v_CH2,i_CH2]
            
        if self.channel=="3":
            return [v_CH3,i_CH3]

        if self.channel=="4":
            return [v_CH4,i_CH4]
1 Like

Hi Christian,

thanks a lot for sharing your insights here about the comparison of the HP4142B and the HP4141B.

So far, it looks like that both instruments are still very similar which is why I suggest that we will create a new driver called “SMU-HP_414xB” that is able to control both models. To do this, the driver could ask for the identification during the ‘initialize’ phase of the driver and then run commands depending on the connected model, e.g. CL or just CL in case of the 41 model.

Do you see any conflicts here? Otherwise, I would prepare a completely new driver with the above name and merge your code with the one from the 42 model and you could test it and make further modificiations if needed.

In case you like to develop or revise more drivers, I would like to hint to our GitHub repository for the instrument drivers:

With the latest SweepMe! version it is now also possible to link the repo in SweepMe!. If you like to use this, please DM me beforehand regarding the legal situation and acknowledgemen of a contribution.

Thanks and best,
Axel

Hi Axel.

Thanks for the feedback.

The command language is very similar but some light differences could make the driver file require a lot if/switch-case statements. It already starts with the “*IDN?” command for the identification of the 42, which needs to be just “ID” for the older 41. Plus, the driver designation “HP 414xB” would imply a suitability for the HP 4145B analyzer which should be different again. Propably easiest to stick to a dedicated 4141B driver.

At least at work, I will not be able to integrate the Repo for the time being. I will contact you independingly regarding my contributions as a private citizen once I start working on certain models I own personally.

Best wishes,
Christian

Thanks for the clarification, makes sense to start with a new driver for the 4141B if even the identification command is different.
We will do some cleanup and also remove the old “multichannel” handling that older SMU drivers still have. Then I will come back to you.

Hi Christian,

I revised the HP4142B driver to fit to our latest standards and created the HP4141B driver.
You can see all changes here:

The drivers do not use the mulitchannel attribute anymore, but define channels via the GUI key “Channel”. Your setting should not break because of that.

Further, the sweep modes are now “Voltage in V” and “Current in A” to fit the scheme used now for all other SMU drivers.

To test, I uploaded “testing” versions to our server that you can download after login with your account.

Please let me know if both drivers work and I will make them publicly available to all users.

Thanks and best,
Axel

Hi Alex.

I will schedule time in the lab for next week to try it out. I just see one issue.
The current github version uses:

    def measure(self):

        # Current
        self.port.write("TI " + self.channel)
        answer = self.port.read()
        # removes first three characters (can be "NAI", "NAV", "XAI", "XAV") and converts value to float
        self.i = float(answer_CH1[3:])

        # Voltage
        self.port.write("TV " + self.channel)
        answer = self.port.read()
        # removes first three characters (can be "NAI", "NAV", "XAI", "XAV") and converts value to float
        self.v = float(answer_CH1[3:])

    def call(self):

        return [self.v, self.i]

I guess using “self.v” within the measure routine will make this variable accessible from the call procedure as well, replacing the global variables I used before.

However, right now, the port read is stored in a variable called “answer” but it is truncated and transferd into self.v/self.i by using the variable “answer_CH1”, a leftover of my modifications to prevent a write-over of values for mutliple channels, that should be superfluent now that the new variables “self.v” and “self.i” can be accessed outside the measure routine again.

If we rename “answer_CH1” to just “answer” for self.v and self.i, the driver should work.

Best wishes,
Christian

1 Like

Thanks for spotting this issue, I have resolved it and uploaded a new testing version.

The variables self.v and self.i are class variables that are once defined available in all functions that have self as argument, so that we do not need to define global variables.

Thanks again for checking the code!

1 Like

Hi Axel.

We tested the current HP 4141B version yesterday and at first, it looked fine.
Then, we took a curve of a simple diode within the range of -0.4V to +1.0V in 0.01V steps and noticed that the measurement points where not equally spaced. We repeated this with a stepsize of 0.1V and it worsened even more.

Uppon further investigation, it turned out that the SMU was not always proceeding with its voltage steps and sticking to previous values sometimes, putting out multiple measurements for these.

I digged into the GPIB protocol and saw something curious: SweepMe did not requested Voltage values in the form of [-0.4, -0.3, -0.2,…,+0.9, +1.0] but created rounding errors, requesting values like -0.299999999999999 etc.
In turn, these values with a lot of trailing numbers behind the decimal point where given over to the GPIB interface without shortening, effectivly overflowing the maximum number of ciffers that are allowed as a parameter for a voltage output on the HP 4141B. The SMU then just skipped the request for a new voltage step and repeated the measurement on the old voltage value, until a “proper” voltage would be requested again without rounding errors and within the ciffer limit.

To prevent this behaviour and to take care of maximum resolution, I implemented a conversion to scientific notation prior to giving the value over to the GPIB bus. The HP4141B GPIB/HPIB programming manual contains just examples with no more than 4 ciffers behind the decimal point, which is why I chose this limitation in the conversion to be on the safe side (I hope).

As a collegue of mine told me that we had observed a slight deficit of these units in accuracy on very low currents compared to modern units when choosing a fast integration speed, I took the time to implement the missing option of choosing the integration speed on the HP 4141B. Note that the manual states that just one integration speed can be set for all channels. Therefore, one should keep this setting identical on all channels used in a SweepMe sequence.

One last comment: there is an additional possibility to take measurements with the HP 4141B that allows for multiple channels to be measured at the same time. This methord works completely different by defining “measurement channels” first with the MC command and then executing an XE command to perform the simultaneous measurement on all defined channels at once. However, this procedure bears two main problems:

  • The result is written in the buffer in just one line and has to be seperated dynamically depending on the amount of channels and their electrical quantity (Current or Voltage), which would mean programming a failure-proof processing on the output
  • Once a channel has been designated as a “MeasurementChannel”, a simple single channel reading cannot be taken anymore. In essence, this conflicts with the current (standard) procedure to perform seperate measuerments on each channel as they appear in the SweepMe sequence. So extreme care would have to be taken that both measurement procedures do not interfere with each other.

For these reasons, I would propose to stick to the current version of performing the measurements.

# This Device Class is published under the terms of the MIT License.
# Required Third Party Libraries, which are included in the Device Class
# package for convenience purposes, may have a different license. You can
# find those in the corresponding folders or contact the maintainer.
#
# MIT License
# 
# Copyright (c) 2024 SweepMe! GmbH (sweep-me.net)
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# SweepMe! driver
# * Module: SMU
# * Instrument: HP 4141B


from pysweepme.EmptyDeviceClass import EmptyDevice


class Device(EmptyDevice):

    def __init__(self):

        EmptyDevice.__init__(self)

        self.variables = ["Voltage", "Current"]
        self.units = ["V", "A"]
        self.plottype = [True, True]  # True to plot data
        self.savetype = [True, True]  # True to save data

        self.port_manager = True
        self.port_types = ["GPIB"]
        self.port_properties = {
            "timeout": 3,
        }

        self.current_ranges = {
            "Auto": "0",
            "1 nA limited auto": "1",
            "10 nA limited auto": "2",
            "100 nA limited auto": "3",
            "1 μA limited auto": "4",
            "10 μA limited auto": "5",
            "100 μA limited auto": "6",
            "1 mA limited auto": "7",
            "10 mA limited auto": "8",
            "100 mA limited auto": "9",
        }

    def set_GUIparameter(self):

        GUIparameter = {
            "SweepMode": ["Voltage in V", "Current in A"],
            "Channel": ["CH1", "CH2", "CH3", "CH4"],
            "RouteOut": ["Rear"],
            "Speed": ["Fast", "Medium", "Slow"],
            "Range": list(self.current_ranges.keys()),
            "Compliance": 100e-6,
            "Average": 1,
        }

        return GUIparameter

    def get_GUIparameter(self, parameter={}):

        self.route_out = parameter['RouteOut']
        self.source = parameter['SweepMode']
        self.port_string = parameter['Port']

        self.irange = self.current_ranges[parameter['Range']]

        self.protection = parameter['Compliance']
        self.speed = parameter['Speed']

        self.average = int(parameter['Average'])

        self.channel = parameter['Channel'][-1]

        self.shortname = "HP4141B CH%s" % self.channel

        # voltage autoranging
        self.vrange = "0"

    def initialize(self):

        unique_DC_port_string = "HP4141B_" + self.port_string

        # initialize commands only need to be sent once, so we check here whether another instance of the same
        # driver AND same port did it already. If not, this instance is the first and has to do it.
        if unique_DC_port_string not in self.device_communication:
            self.port.write("ID")  # identification
            identifier = self.port.read()
            # print("Identifier:", identifier)

            self.port.write("*RST?")  # reset to initial settings

            self.port.write("BC")  # buffer clear

            self.port.write("FMT 2")  # use to change the output format

            # if initialize commands have been sent, we can add the unique_DC_port_string to the dictionary that
            # is seen by all drivers
            self.device_communication[unique_DC_port_string] = True

    def configure(self):

        # The command CN has to be sent at the beginning as it resets certain parameters
        # If the command CN would be used later, e.g. during 'poweron', it would overwrite parameters that are
        # defined during 'configure'
        self.port.write("CN " + self.channel)  # switches the channel on

        
        if self.speed == "Fast": # 1 Short (4 measurements with 4 per PLC) preconfigured selection Fast
            self.port.write("IT 1")
            #self.port.write("AAD %s,0" % self.channel)
        elif self.speed == "Medium": # 2 Medium (16 measurements with 16 per PLC) preconfigured selection Normal
            self.port.write("IT 2")
            #self.port.write("AAD %s,1" % self.channel)
        elif self.speed == "Slow": # 3 Long (256 measurements with 16 per PLC) preconfigured selection Quiet
            self.port.write("IT 3")
            #self.port.write("AAD %s,1" % self.channel)
        

    def unconfigure(self):
        self.port.write("IN" + self.channel)
        # resets to zero volt
        # self.port.write("DZ")

    def poweron(self):
        pass
        # In a previous version, the CN command was sent here. However, this leads to a reset of all parameters
        # previously changed during 'configure'
        # Therefore, the CN command should not be used here, but has been moved to the beginning of 'configure'

    def poweroff(self):

        # do not supply channel numbers as arguments, will not work on HP4141B otherwise
        self.port.write("CL")  # switches all channels off
        # TODO: "CL" is called for each channel so more often than needed. In future, we can add a mechanism to do it
        # only once by using self.device_communication dictionary.

        # Please note that because CL takes no channel number and switches off all channel, also channels are powered
        # off that should stay if they are even present in the next active branch of SweepMe! sequencer.

    def apply(self):

        value = str("{:.4E}".format(self.value))

        if self.source.startswith("Voltage"):
            self.port.write("DV " + self.channel + "," + self.vrange + "," + value + "," + self.protection)

        if self.source.startswith("Current"):
            self.port.write("DI " + self.channel + "," + self.irange + "," + value + "," + self.protection)

    def measure(self):
    
        # Current
        self.port.write("TI " + self.channel)
        answer = self.port.read()
        # removes first three characters (can be "NAI", "NAV", "XAI", "XAV") and converts value to float
        self.i = float(answer[3:])

        # Voltage
        self.port.write("TV " + self.channel)
        answer = self.port.read()
        # removes first three characters (can be "NAI", "NAV", "XAI", "XAV") and converts value to float
        self.v = float(answer[3:])

    def call(self):

        return [self.v, self.i]


    """
    Enables channels CN [chnum ... [,chnum] ... ]
    Disables channels CL [chnum ... [,chnum] ... ]
    Sets filter ON/OFF [FL] mode[,chnum ... [,chnum] ... ]
    Sets series resistor ON/OFF [SSR] chnum,mode
    Sets integration time
    (Agilent B1500 can use
    AAD/AIT instead of AV.)
    [AV] number[,mode]
    [AAD] chnum[,type]
    [AIT] type,mode[,N]
    Forces constant voltage DV chnum,range,output
    [,comp[,polarity[,crange]]]
    Forces constant current DI
    Sets voltage measurement
    range
    [RV] chnum,range
    Sets current measurement
    range
    [RI] chnum,range
    [RM] chnum,mode[,rate]
    Sets measurement mode MM 1,chnum[,chnum ... [,chnum] ... ]
    Sets SMU operation mode [CMM] chnum,mode
    Executes measurement XE
    """

Best wishes,
Christian

PS: the HP 4141B demands the “E” in the scientif notation to be a capital letter. These 80s machines were a bit “strict” :smile:

1 Like

Hi Christian,

thanks a lot for your further contrubution and insights about the HP4141B.

As you write it is a typical problem for older instruments to not be able to handle floating numbers with many decimal positions.

I have included your changes as is and uploaded the first “live” version of this driver that everybody can access.

I checked the HP4142B and HP4145 drivers. I added it as well to the 4142B driver. The HP4145 driver already had the same solution included.

I know it has been some work to fix the issue, but I am really happy that the drivers improve this way step by step and on demand.

Thanks and best, Axel

1 Like

Regarding your comment about the simultaneous multi-channel reading, there is a solution in SweepMe! drivers:

SweepMe! drivers have a dictionary self.device_communication that is the same object for all driver instances and it can be used to let driver instances “communicate” with each other:

Typically, drivers register a key there with further information. All driver instances of the same driver cann check there whether other instances already left some information and do some further steps depending on it.

A very similar example is the Agilent 415x driver

Here, the EX command is used as well to trigger a measurement for list sweeps of multiple channels.

However, the implementation might be a bit more complicated and could require some support from us that goes beyond the help that we can give here in the forum.

As only one instance reads back the measurement data for all instances of the driver, it must be stored again in the self.device_communication dictionary to enable the other instances to get their data and return a data set per channel/driver instance.

1 Like

Thanks for the insights, Axel. I think for the time being, we will be fine with the current solution.

1 Like