PyFixate / Fixate

Framework for hardware test fixtures and automatic test environments
MIT License
22 stars 16 forks source link

Add DMM Range Handler Functions #205

Open Jasper-Harvey0 opened 3 months ago

Jasper-Harvey0 commented 3 months ago

In the interest of improving compatibility between different DMM models, I have had an idea to add functions that handle all the logic behind setting ranges on the DMM.

The idea is to add all the available measurement ranges for a DMM into its driver (rip it straight out of the datasheet). This allows us to sanitize any requests to set the range on the DMM. Part of the motivation for this is because of the differences in current ranges for the high / low range ports on Fluke / Keithley DMMs. The Fluke has a low range port that goes up to 400mA, whereas the Keithley's low range can go to 3A. Any jigs currently in production have been deployed with the assumption that they are using a Fluke DMM. Handling the range request will allow us to select the correct port on the Keithley to improve compatibility.

Jasper-Harvey0 commented 3 months ago

This would obviously be reflected in the Fluke driver should this idea be accepted.

clint-lawrence commented 3 months ago

I'm just going to brainstorm some idea without too much thought

There are probably other options too. I'm tempted to think this might benefit from a white board session?

clint-lawrence commented 3 months ago

I think this is good concept in general. I'm not sold on the idea you've outlined where you silently force the range change. But I'm hopeful we can come up with a way to do this that makes it generally do the "obvious" things to be compatible between both DMMs, yet still be explicit and allow some flexibility.

For example, if I was intentionally using the keithly driver and it switch ranges on me like that, I'd find it very surprising.

The other thought in the back of my head is if we're should be really targeted at the current range (where there is a clear concrete impact) vs having something general which maps out the ranges for all measurement functions.

Jasper-Harvey0 commented 3 months ago

Good ideas. My initial implementation was based on the idea that we could make driver changes and no test script changes. The bad thing about it is it kind of locks in the behavior of the Fluke for all other DMM drivers which is not nice. There is also the fact that it silently modifies things without telling you.

Jasper-Harvey0 commented 3 months ago

I think I might be leaning towards returning an enum for the high / low ports. This allows test scripts to check what port is being used and do appropriate jig switching, and doesn't break any existing jigs. It will look something like

class Fluke8846A(DMM):
    ...
    self.current_port_threshold = 400e-3 # Changeover range for high and low ports.
    ...

    def current_dc(self, _range=None):
        self._set_measurement_mode("current_dc", _range)
        if _range > self.current_port_threshold:
            # This will be the high current port
            return CurrentPort.HIGH
        else:
            # This will be the low current port
            return CurrentPort.LOW

jigs can then do:

if dm,dmm.current_dc(_range=3) == 0
    # Switch jig to low current port
else:
    # Switch jig to high current port

Or in the case of some existing jigs where changing the switching is not an option, we can do something like the below:

# Jig is restricted to high range port
measurement = 2.5 # Want to measure 2.5 A
crossover_range = dm.dmm.current_port_threshold # 3 A for Keithley, 0.4 A for Fluke
if crossover_range < measurement:
    dm.dmm.current_dc(_range=measurement)
else:
    dm.dmm.current_dc(_range=crossover_range*1.2) # Force high range port
josh-marshall-amp commented 3 months ago

I think I might be leaning towards returning an enum for the high / low ports. This allows test scripts to check what port is being used and do appropriate jig switching, and doesn't break any existing jigs. It will look something like


class Fluke8846A(DMM):
    ...
    self.current_port_threshold = 400e-3 # Changeover range for high and low ports.

Delete the self here, which I think you meant to do anyway. It can/should be a class variable.

def current_dc(self, _range=None):
    self._set_measurement_mode("current_dc", _range)
    if _range > self.current_port_threshold:
        # This will be the high current port
        return CurrentPort.HIGH
    else:
        # This will be the low current port
        return CurrentPort.LOW

jigs can then do:

if dm,dmm.current_dc(_range=3) == 0

Switch jig to low current port

else:

Switch jig to high current port


Where have you put the `CurrentPort(enum)`? I would put it as a class variable.
This can then be written as `if dm,dmm.current_dc(_range=3) == dmm.CurrentPort.HIGH`... shouldn't be comparing to 0.

More abstractly... Why are we putting the responsibility on the jig test code rather than the driver? This is a manual intervention from the test operator to switch ports?

Side-nitpick: _range is anti-Pythonic, but I presume that's a legacy from early days. Should be range_.

Or in the case of some existing jigs where changing the switching is not an option, we can do something like the below:

# Jig is restricted to high range port
measurement = 2.5 # Want to measure 2.5 A
crossover_range = dm.dmm.current_port_threshold # 3 A for Keithley, 0.4 A for Fluke
if crossover_range < measurement:
    dm.dmm.current_dc(_range=measurement)
else:
    dm.dmm.current_dc(_range=crossover_range*1.2) # Force high range port

Context that I don't know much about the legacy stuff... I don't like this; too magical. If the test operator has to switch plugs manually to change range between HIGH and LOW, then that setting should always be an explicit action from the jig test code at the same time, given we know the expected measurement in advance.

If we measure something in an inappropriate range because the port is meant for a higher current then we can log a warning, but if we attempt to measure say 2.5A and the range is set to 0.4A that should raise a DamagingRangeError exception or something.

josh-marshall-amp commented 3 months ago

And this was kinda only distantly related @Jasper-Harvey0, but a few days back I was thinking as to what a refactor would look like and my concept riffing off your code ended up like this.

The beauty of Python enums vs C is that you have easy access to both name and value at all times. In that sense they're almost like a bidirectional, const dictionary.

Note the enum is on the class. Then the Fluke could do the same, if you did that then the _select_range() and _get_ranges() could get moved to the DMM superclass.

Now of course this is once again not taking legacy issues into account, and the other guys have probably wanted to do this in the past but it's a big job. 🙃

class Keithley6500(DMM):
    class _ranges(enum.Enum):
        current_dc = ( 10e-6, 100e-6, 1e-3, 10e-3, 100e-3, 1, 3, 10,)
        current_ac = (100e-3, 1e-3, 10e-3, 100e-3, 1, 3, 10)
        voltage_dc = (0.1, 1, 10, 100, 1000)
        voltage_ac = (100e-3, 1, 10, 100, 750)
        resistance = (1, 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, 100e6)
        temperature = ()  # Empty. No ranges for temperature
        frequency = ( 300e-3,)  # No adjustable range for frequency. Just put maximum range here.
        period = (3.3e-6,)  # No adjustable range for period. Just put maximum range here.
        continuity = (1e3,)  # No selectable range for continuity. Put maximum range here.
        capacitance = (1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6)
        diode = (10,)  # No selectable range for diode. Default is 10V

    def _select_range(self, value):
        if value is None:
            return None

        ranges = self._get_ranges()
        for i in ranges:
            if abs(value) <= i:
                return i
        raise ParameterError(
            f"Requested range '{value}' is too large for mode '{self.mode}'"
        )

    def _get_ranges(self):
        """
        Returns a tuple of available ranges for the current mode
        """
        if self.mode is None:
            raise InstrumentError("DMM mode is not set. Cannot return range")

        return self._ranges[self.mode].value # DMMRanges.mode_to_range[self.mode]
Jasper-Harvey0 commented 3 months ago

Yep current_threshold will be a class variable. I guess my example code was a bit too quick and dirty, comparing to 0 is definitely not the right way.

~~For some context. Up until about a year ago, we have only ever used a Fluke DMM. This has a low range current port that can measure up to 400mA, and a high range port for measurements up to 10A. The new Keithley DMM has a low range port that goes up to 3A, and a high range that measures up to 10A. This means that when a test jig designed for the Fluke wants to measure, say 2.5 A, it will be passing current through the high range port on the DMM. But the Keithley would still select the 3A low range port (when given _range = 2.5)~~ I realized I just repeated my first comments in the description.

I think putting the responsibility on the test jig code is needed because it needs to know what current path to use (high or low range ports). And some jigs that are already designed may not even have the ability to swap ports. This is even more tangled because we cannot be designing the test code to use either fluke or keithley, we need to assume either can be used.

Maybe this just boils down to an education thing? - Make sure everyone that writes test scripts are aware that current measurements are scary and you should be careful and test with all available DMMs... After all, we almost never care about resolution in these measurements.

Also I am totally not attached to any solution, so whatever works I am happy with.

clint-lawrence commented 3 months ago

More abstractly... Why are we putting the responsibility on the jig test code rather than the driver? This is a manual intervention from the test operator to switch ports?

Because the driver doesn't know about (and shouldn't know about) how to switch signals in the jig. But the driver can provide a useful hint to the test script so it's knows what terminals to use.