Git Product home page Git Product logo

Comments (15)

RickPattonAlicat avatar RickPattonAlicat commented on July 28, 2024 1

Hi dom-insytesys and patrickfuller, Alicat Test Engineer here! Still new to Git, so please excuse any newbie mistakes, but I do have some comments on the issues discussed above.

Regarding setpoint precision, the _set_setpoint() function here assumes all devices have 2-digit floating point precision, which may not be the case depending on device range or selected flow units. IMO, a reliable way of determining the precision of the setpoint is to simply get() the device state, and parse out the number of decimals, using that value to format your setpoint command string.

Regarding register map changes, all registers should be backwards compatible across the history of our firmware, though many registers have been added over the years, so higher registers (with similar functionality to lower registers) may not exist on older devices. Major changes occurred with a PCB hardware change starting with S/N# 80000 and beyond, where many of the perhaps more familiar ASCII commands were added. Prior to S/N# 80000, the highest register available was Register 79. Another major change occurred starting with firmware version 6vXX and beyond, and again a smaller addition with 7vXX.

TL;DR... self.control_point can be determined by querying Register 20 instead. On more recent firmware builds with Register 122, Register 122 and Register 20 are linked asynchronously, so changing one will change the other. Per this, I recommend redefining:

    # Mass Flow = +1024, Vol Flow = +768, Pressure = +256
    registers = {'flow': 0b0000010000000000,
                 'volume': 0b0000001100000000,
                 'pressure': 0b0000000100000000}

See changes to other functions and added _get_control_precision() below:

    def _set_setpoint(self, setpoint, retries=2):
        """Set the target setpoint.
        Called by 'set_flow_rate' and 'set_pressure', which both use the same
        command once the appropriate register is set.
        """
        self._test_controller_open()

        command = '{addr}S{setpoint:.{decimals}f}\r'.format(addr=self.address,
                                                   setpoint=setpoint,
                                                   decimals=self._get_control_precision())
        line = self._write_and_read(command, retries)

        # Some Alicat models don't return the setpoint. This accounts for
        # these devices.
        try:
            current = float(line.split()[-2])
        except IndexError:
            current = None

        if current is not None and abs(current - setpoint) > 0.01:
            raise IOError("Could not set setpoint.")

    def _get_control_point(self, retries=2):
        """Get the control point, and save to internal variable."""
        command = '{addr}R20\r'.format(addr=self.address)
        line = self._write_and_read(command, retries)
        if not line:
            return None
        value = int(line.split('=')[-1])
        try:
            return next(p for p, r in self.registers.items() if value & r == r)
        except StopIteration:
            raise ValueError("Unexpected register value: {:d}".format(value))

    def _get_control_precision(self, retries=2):
        """Get the precision of the control setpoint, and save to internal
        variable.
        """
        dataframe = self.get(retries=retries)
        if not dataframe:
            return None

        try:
            decimals = len(str(dataframe["setpoint"]).split('.')[1])
        except IndexError:
            decimals = 0
        return decimals

    def _set_control_point(self, point, retries=2):
        """Set whether to control on mass flow or pressure.
        Args:
            point: Either "flow" or "volume" or "pressure".
        """
        if point not in self.registers:
            raise ValueError("Control point must be 'flow' or 'volume' or 'pressure'.")

        # Get current device control state.
        command = '{addr}R20\r'.format(addr=self.address)
        line = self._write_and_read(command, retries)
        if not line:
            raise IOError("Could not detect current device control state.")
        curr_reg = int(line.split('=')[-1])

        # Subtract current control bitvalue; add new control bitvalue.
        reg = curr_reg - self.registers[self.control_point] + self.registers[point]
        command = '{addr}W20={reg:d}\r'.format(addr=self.address, reg=reg)
        line = self._write_and_read(command, retries)

        value = int(line.split('=')[-1])
        if value & reg != reg:
            raise IOError("Could not set control point.")
        self.control_point = point

Regarding flow units selected, this can be queried with an "FPF" command (5 = Mass Flow, 4 = Vol Flow, 3 = Temperature, 2 = Absolute Pressure). E.g.:

A FPF 5 returns
"A [fullscale] [unitNumber] [unitLabel]", or in a specific example:
"A 10.000 7 SLPM"

In fact, a more general solution to the setpoint precision and units question could be to create a UnitConverter class to convert between potential mismatches of input flow values to device flow values, and vice versa, depending on the current device configuration and whichever units are important to you (e.g. SLPM = SCCM/1000.0). Depending on whether or not you want to allow a user to change the device configuration after loading into your script, you can load the device configuration during __init__ or query device configuration before sending certain commands (like setpoint, so you know the current device flow units).

from alicat.

patrickfuller avatar patrickfuller commented on July 28, 2024

I've run into this as well, and I'll explain why the library currently works the way it does.

Explanation

Alicat has a main command (pg42 of the manual) that forces the decimal truncation, e.g. as15.44. This command is stable across Alicat versions. It works with older and newer ones, even if lower-level commands change.

Below this command is a low-level API that allows you to set specific memory addresses. This usually follows the pattern aw122=60000, which means "Unit A, Write to address 122 the value 60000". Each address, or "register", is 16bit so supports 0-65535. In newer Alicats, they included the shorthand a60000 to write to the setpoint register.

Here's the frustrating part - the register shorthand doesn't work on all Alicats. Furthermore, the registers change between Alicat versions, ie. address 122 may be 65 on an older Alicat.

Proper Fix

If I had time and Alicat support, I'd probably compile a big list of Alicat versions + register maps. I'd query the device on connection to figure out what version it's running, and then be able to automatically expose an appropriate python API while hiding the memory addresses.

Short-term Fix

What if you change the units from SLPM to sccm? It should be doable on the device front panel.

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

Thanks for the speedy and informative reply!

I wasn't aware that changing the units on the front panel affected the units used to program the device over serial comms. I'll give that a try. The comments in set_flow_rate() reads "flow: The target flow rate, in units specified at time of purchase", which makes it sound like this is fixed and not user-configurable.

from alicat.

patrickfuller avatar patrickfuller commented on July 28, 2024

Agreed, but I know we have some 1SLPM controllers that are being controlled by sccm. If it doesnโ€™t work, then Iโ€™ll show you how to monkey patch the library to use the register.

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

Just read some the manual that you linked to. On page 20, it says that you can set "Button engineering units" or "Device engineering units". If I understand it correctly, the former affects the front panel only, the latter changes both front panel and serial communications.

So that should be an acceptable workaround. But in order to write reliable code (i.e. that operates consistently whether or not the front panel has been adjusted correctly), I really need to be able to query and configure the units via the serial comms. There's nothing in the "Serial Command Guide" (page 43) that describes how to do this. Although it does say helpfully "If you have need of more advanced serial communication commands, please contact Alicat." :-)

from alicat.

patrickfuller avatar patrickfuller commented on July 28, 2024

@RickPattonAlicat thanks for replying! I think there's a lot we could do with this Alicat library (although it'd be a gradual nights-and-weekends project for me).

After having run this library for a few years on a variety of devices, I'm pretty convinced that the most stable route would be to directly read/write registers. The main serial command varies between devices (number of fields, length per field), which is fine most of the time but occasionally leads to misreads.

In my mind, the ideal driver would request a batch of data on __init__, including version, max flow, units, meter vs controller, selected gas, available gases. This would get cached and be accessible via __repr__ for interactive testing. If we have the max flow, we'd then be able to calculate and write the 16-bit setpoint register to the highest possible accuracy without changing the python API. Same with custom gases and better error messages for unavailable features.

Do you have more documentation on available registers? Also, is there a way to read multiple registers in one command? Happy to move this conversation to email or phone!

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

@RickPattonAlicat That's super useful information. Thank you!

I'll give the "FPF" command a shot to pull out the current units and full scale. If nothing else, that's preferable to what I'm doing now, which is to guess max flow and units based on the MFC model number. I assume the former is reliable, but latter could potentially be changed via front panel.

On the subject of usable precision, what is the downside of setting the desired mass flow setpoint as an integer? i.e. round(setpoint * max flow / 64000) It seems to me that this sidesteps any issue of how many digits of precision are appropriate.

from alicat.

RickPattonAlicat avatar RickPattonAlicat commented on July 28, 2024

@dom-insytesys This could be preferable in many circumstances. For example, our devices approximately pre-2018 would typically have a 10000 count max precision on the fullscale flow (with some exceptions depending on some customer ordering preferences). In most cases, you would get exact precision using the 64000 count register, but on some edge cases, you may have a 1-count rounding error. In general there can be some configurations with a not-so-round number of fullscale flow counts, or even an extra digit of decimal resolution (e.g. 10.000 SCCM), so some devices would result in a loss of precision. Additionally, with most devices since 2018, we've typically been able to expand the usable resolution by a full digit, so there may be a loss in precision in, for example, a 75.000 SLPM device, with a 64000-count setpoint resolution of 0.0012/count.

As a general practice, we like to encourage using ASCII commands where possible to avoid accidentally overwriting the wrong register. While that is less of a concern using a pre-built package like this, where the user isn't actively typing register values into a console, I still wanted to raise the point.

Another, less obvious reason would be to avoid excessive writes to the EEPROM. I believe our EEPROMs have a lifetime on the order of 1-million writes (don't quote me on that ๐Ÿ˜… ), but if you're running a complicated script with a rapidly-varying setpoint, those writes to an EEPROM register could accumulate quickly. We have a register setting that can disable saving setpoints to the EEPROM to avoid burning it out in such a scenario.

So perhaps a more flexible solution would allow the 64000-scale setpoint with one function/property, and the floating-point setpoint in another function/property? I'll get with @patrickfuller over email to discuss further register map documentation, etc.

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

@RickPattonAlicat I don't really understand your reasoning: configuring the setpoint as integer doesn't involve explicitly writing to a register. According to page 42 of the flow controller manual, you just send the integer as ASCII digits. This seems to work as expected in my tests. Weirdly, there are no command characters at all, just a device address character, so in a way this is the default way of programming the controller. There doesn't seem to be any way to mess up and do the wrong thing.

I don't understand your resolution examples, either. If flow controller is a 75.000 SLPM device, I'm assuming that means that configuring setpoint as a float would set it to the nearest 0.001 SLPM. And as you observe, configuring setpoint as integer would round to the nearest 0.00117 SLPM, which is pretty much exactly the same thing.

Any sane application should not blindly rely on the flow (or pressure) setpoint being set exactly as configured. What we do is to read the MFC response, and use that as the recorded setpoint. Either way seems vulnerable to rounding errors.

On which subject, @patrickfuller, we ended up bypassing the set_flow_rate() command in numat/serial.py because it doesn't return the configured setpoint. It just raises an IOError exception if the difference between target setpoint and actual setpoint is outside an arbitrary tolerance. That's handy, but ideally (at least, the way we re-wrote the function) it would return either the raw response string, or a parsed dictionary, just as get() does. Since the function is already retrieving this information, there doesn't seem to be much downside.

If we hadn't modified your code to return the actual configured setpoint, I would have never noticed that our experimental errors were due to the setpoint being truncated much more than I expected.

from alicat.

patrickfuller avatar patrickfuller commented on July 28, 2024

@dom-insytesys this is also something that's not consistent across alicats. Not all models return a full line on set (see comment).

It's bad practice to have a library that behaves differently on different device versions, so this library drops the response if it exists. The setpoint check was a nice bonus, allowing us to do something with the data without making the API ambiguous. (The other option would be to have old controllers run a silent get but that just moves the comm overhead to older devices).

Ideally, we'd be able to handle these quirks with a table of device versions, but, until then, we need to balance features with back compatiblity.

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

@patrickfuller That's a good point. But in a way, what the library does is already device-dependent. On devices that return a response, it silently performs a check on that response. Also, (and I don't have such a device to test) I assume that on older controllers, the _readline() spends a lot of time trying to read a response before timing out. So there's already a significant, unnecessary comms overhead.

In an ideal world, at instantiation, there would be a check of MFC capabilities, and for older devices, the _readline() would never be tried, and instead a get() or a specific "FPF" query would be used instead. Perhaps with an optional "no_check" flag to enable the user to skip this if they don't care.

Either way, _set_setpoint() already captures and parses the MFC response. And the current API doesn't return anything so if you changed the code to return the response in some form, it shouldn't break any existing code. And you could easily return None if the MFC is and old device that doesn't auto-respond with its new status.

@RickPattonAlicat: Is there a reliable way to query an Alicat MFC to find out if it will return a status response after a setpoint is configured? Also, is there a way to disable this feature? The reason I ask is that in our test rig, we have several MFC's that we would like to configure to a new setpoint as close to simultaneously as possible. Using Patrick's set_flow_rate() function, the _write_and_read() spends about 1 millisecond in the write() part, and then about 30-50 ms in the _readline() portion. In an ideal world, I would like to separate the two parts: i.e. send a new setpoint to all the MFC's, and only check their status after all the setpoints have been sent. At which point, if any didn't receive the setpoint correctly (a situation I have yet to encounter in my testing), the test runner software can re-issue the setpoint command. Right now, I've modified our code to work this way, but the subsequent get() commands often encounter communication errors, presumably because the MFC's are already trying to send back their status without waiting to be prompted to do so. My code retries the get() and that consistently succeeds the second time around, so ultimately the setpoints get confirmed, but it feels ugly to be dropping/mangling the automatic response. It would be nice either to disable the auto-response, or to issue the new setpoint with a different command that doesn't trigger the response.

from alicat.

RickPattonAlicat avatar RickPattonAlicat commented on July 28, 2024

@dom-insytesys Sorry, I think I misunderstood your original question regarding the 64000 setpoint command. I thought you were suggesting to write directly to the setpoint register via a AW[reg]=[value] command, rather than the A64000-type command, which does not write to EEPROM under the register setting I mentioned, and of course couldn't overwrite the wrong register.

Agreed, under the 0.00117/count example, it's "pretty much" the same thing, but not exactly the same thing. For example, at 75/64000 SLPM per count, it would be impossible to send a 0.003 SLPM setpoint, since A2 results in 0.002 and A3 results in 0.004. Across the full range in this example, there would be 11000 specific setpoint values that could not be reached by the command. Likely not an issue in most use cases, but could result in some frustration and an additional issue thread down the road.

With your multiple device setpoints question, if you're giving the same setpoint to all, you can use an asterisk to talk to all connected devices at once. Perhaps @patrickfuller would consider adding an ignore_response parameter in _write_and_read(), which would skip the line = self._readline() statement and just return None instead. Below is a function I use within my own in-progress API to send a generic command, along with various parameters for modifying its behavior:

    def _sendcommand(self, cmd, wait=0.075, verbose=0, read=1, flush=0, clean=1):
        """Sends command to device per alicat and serial formatting
        Waits a number of seconds specified by [wait].
        If verbose=1, print a copy of the command to the terminal.
        If read=0, do not attempt to read the buffer.
        If flush=1, reset the buffer after waiting [wait] seconds.
        If clean=0, do not clear the buffer prior to sending the command.
        """
        if "*" not in cmd:
            cmd = self._device_id + cmd

        if clean:
            self.ser.reset_input_buffer()
        if verbose:
            print(cmd)
        self.ser.write(cmd.encode('utf-8') + b'\r')
        time.sleep(wait)
        if flush:
            self.ser.reset_input_buffer()
        if read==0:
            return ''
        out = self.readbuffer() #defined elsewhere, allows for multi-line responses
        return out

For example, to achieve what you want, you could do:

for dut in duts:
    dut._sendcommand("S{}".format(setpoint), wait=0.001, read=0, clean=0)
time.sleep(0.075)
# Remember to clear the receiving buffer after all the setpoints are sent.
duts[0].ser.reset_inputbuffer()

I don't believe there is a "disable serial response" mode, but let me double-check.

from alicat.

RickPattonAlicat avatar RickPattonAlicat commented on July 28, 2024

@dom-insytesys confirmed, there's no way to disable the device from returning a dataframe in response to a setpoint command. I believe the best way to achieve this is either the asterisk command, e.g. *S1.234, or by modifying _write_and_read() to allow the user to specify a parameter to ignore the response, and just return to the parent as soon as the command is sent.

from alicat.

dom-insytesys avatar dom-insytesys commented on July 28, 2024

@RickPattonAlicat Thanks again for your assistance.

Sending same setpoint to all MFC's is, unfortunately, not an option. But it sounds like the reset_inputbuffer() might fix the garbled response to get(), which I assume is caused by all the MFC's returning their dataframes and my code not reading them.

from alicat.

patrickfuller avatar patrickfuller commented on July 28, 2024

@dom-insytesys you're stuck with synchronous comm if you use Alicat's addressing, but you could always use a serial hub instead. Dedicated hubs would let you asynchronously request/respond. Some detail is here and this is how we run most of our systems.

from alicat.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.