-
Notifications
You must be signed in to change notification settings - Fork 30
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
Pull request for Alicat Python Library #15
Conversation
Thanks for the PR! Reviewing will be a fun Friday afternoon distraction so I'll get you something back later today. Regarding your comments on registers, I've always felt that this driver in general would be better if we moved away from using the space-separated API and relied more heavily on registers. That is, I like the path you're going down there and would be very open to more of it. Thanks again! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool! I have a lot of little comments that I hope you find educational, and I'm happy to discuss more if needed.
Hope this helps!
README.md
Outdated
@@ -80,10 +80,48 @@ You can also set the gas type and flow rate / pressure. | |||
|
|||
```python | |||
flow_controller.set_gas('N2') | |||
flow_controller.set_gas(8) # Optionally set a gas by it's number; find the full gas table on page 52 of the Alicat manual. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove apostrophe - "its" - and move out from a code comment to the README itself. Something like "gases can be set by either their name or their index number" before this code block.
README.md
Outdated
flow_controller.set_flow_rate(1.0) | ||
flow_controller.set_pressure(20) | ||
``` | ||
|
||
For firmwave 5v and greater, create and set gas mixes using COMPOSER software loaded into the device. Mixes can contain up to five gases, and are stored in gas indices 236-255. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ware, not wave.
Is there a way to read the firmware version through the serial commands? We could use this on init to set features and raise errors if someone tries to use an unsupported function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind, you already do this and it's great.
flow_controller.create_mix(mix_no=236, name="Mix1", gas1="N2", percent1=50, gas2="Ar", percent2=50) | ||
flow_controller.set_gas(236) | ||
flow_controller.delete_mix(236) | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool!!
flow_controller.write_PID_P(4000) | ||
flow_controller.write_PID_D(10) | ||
flow_controller.write_PID_I(4000) | ||
print(flow_controller.read_PID()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also cool! I've been meaning to add PID tweaking to this code since day 1, so it's great to see it here.
alicat/serial.py
Outdated
' {g5}\r'.format(addr=self.address, shortName=name, mixNumber=mix_no, p1=percent1, | ||
g1=self.gases.index(gas1), p2=percent2, g2=self.gases.index(gas2), | ||
p3=percent3 if gas3 is not None else "", g3=self.gases.index(gas3) if gas3 is not None else "", p4=percent4 if gas4 is not None else "", | ||
g4=gas4 if gas4 is not None else "", p5=percent5 if gas5 is not None else "", g5=self.gases.index(gas5) if gas5 is not None else "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also be cleaner and made to not trail so many spaces.
gas_list = ' '.join(' '.join(self.gases.index(gas), percent) for gas, percent in gases.items())
command = ' '.join(self.address, name, mix_no, gas_list) + '\r'
Double join read a little awkward but it avoids you repeating yourself. Could replace the inner join with a small format string as well to try helping readability.
command = '{addr}$$C\r'.format(addr=self.address) | ||
self._write_and_read(command, retries) | ||
|
||
def read_PID(self, retries=2): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might get yelled at by pycodestyle
for caps in the function name. The general rule is code style supersedes acronym capitalization, so it could push for read_pid
instead.
Personally, I think it's fine either way.
alicat/serial.py
Outdated
if spl[3] == '2': | ||
loop_type = 'PD2I' | ||
elif spl[3] == '1' or spl[3] == '0': | ||
loop_type = 'PD/PDF' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick here. I'd write as
loop_type = ['PD', 'PDF', 'PD2I'].index(int(spl[3]))
This way, you'll get better errors when spl[3]
is malformed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Readability here. Do in two lines even if one would work.
loop_num = int(spl[3])
loop_type = ['PD/PDF', 'PD/PDF', 'PD2I'].index(loop_num)
alicat/serial.py
Outdated
if looptype == 'PD/PDF': | ||
command = '{addr}$$w85=1\r'.format(addr=self.address) | ||
elif looptype == 'PD2I': | ||
command = '{addr}$$w85=2\r'.format(addr=self.address) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
command = '{addr}$$w85={loop_num}\r'.format(
addr=self.address,
loop_num=['', 'PD/PDF', 'PD2I'].index(looptype)
)
alicat/tcp.py
Outdated
else: | ||
num = False | ||
|
||
if num is True: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments all apply here.
I'm thinking about rewriting this so we don't have to repeat ourselves in the code, but unfortunately should copy it all over for now.
Thank you for the feedback! I will work on making the changes as time allows. Thanks again! |
Hi Patrick! I'm sorry this took so long.... some things happened when COVID started really affecting us and I wasn't allowed to work on this for a long time. I've dusted this off and made all the changes you requested now that I've finally been able to work and this and got all my other projects out of the way. If you wouldn't mind taking another look, I'd really appreciate it. Happy new year! |
@marinapalese Happy new year and no worries! It's been a wild couple of years for all of us. I'll review ASAP. Also - any chance you're able to test this driver on an array of MFCs? (We could probably string something together at our company but I figured you might have access to a nicer test setup!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marinapalese hope this helps! I'm being extra nitpicky in the hope of showing off the "pythonic" way of doing things. None of my comments really matter if we can test and show this works on a few live devices, but I'm still hoping you find it helpful to read through!
alicat/serial.py
Outdated
del values[-1] | ||
|
||
print(values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete print statement.
If you want to print something out in production code, I recommend the logging module.
import logging
logger = logging.getLogger('alicat')
logger.debug(f'Read values: {values}')
This allows the end user to turn the print statements on and off depending on what they're looking for.
This all said, I don't think that's the intent of this print statement. If not, it should just be deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I was just trying to quickly check something here and forgot to delete that. Thank you for telling me about the logging module regardless!
alicat/serial.py
Outdated
|
||
if number != reg46_gasbit: | ||
raise IOError("Cannot set gas.") | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete pass
else: | ||
return self._set_gas_name(gas, retries) | ||
|
||
def _set_gas_number(self, number, retries): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend adding a docstring to say what this is doing. Something like:
"""Set flow controller gas type by number.
See the Alicat manual for usage.
"""
reg46 = self._write_and_read('{addr}$$R46\r'.format( | ||
addr=self.address | ||
), retries) | ||
reg46_gasbit = int(reg46.split()[-1]) & 0b0000000111111111 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
raise IOError("Cannot set gas.") | ||
pass | ||
|
||
def _set_gas_name(self, name, retries): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstring here too. Something like
"""Set gas type by name.
See supported gases in `FlowController.gases`.
"""
alicat/serial.py
Outdated
def lock(self, retries=2): | ||
"""Lock the display.""" | ||
self._test_controller_open() | ||
command = '{addr}$$L\r'.format(addr=self.address) | ||
self._write_and_read(command, retries) | ||
|
||
def unlock(self, retries=2): | ||
"""Unlock the display.""" | ||
self._test_controller_open() | ||
command = '{addr}$$U\r'.format(addr=self.address) | ||
self._write_and_read(command, retries) | ||
|
||
def tare_pressure(self, retries=2): | ||
"""Tare the pressure.""" | ||
self._test_controller_open() | ||
|
||
command = '{addr}$$PC\r'.format(addr=self.address) | ||
line = self._write_and_read(command, retries) | ||
|
||
if line == '?': | ||
raise IOError("Unable to tare pressure.") | ||
|
||
def tare_volumetric(self, retries=2): | ||
"""Tare volumetric flow.""" | ||
self._test_controller_open() | ||
command = '{addr}$$V\r'.format(addr=self.address) | ||
line = self._write_and_read(command, retries) | ||
|
||
if line == '?': | ||
raise IOError("Unable to tare flow.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are great features. Thanks for adding!
alicat/serial.py
Outdated
if any([self.control_point is not None and | ||
self.control_point == 'abs pressure', | ||
self.control_point is not None and | ||
self.control_point == 'gauge pressure', | ||
self.control_point is not None and | ||
self.control_point == 'diff pressure']): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, how about
if self.control_point in ['abs pressure', 'gauge pressure', 'diff pressure']:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! For some reason I remember trying this and having trouble with it. Just changed it and no issues with any device.
alicat/serial.py
Outdated
if any([self.control_point is not None and | ||
self.control_point == 'mass flow', | ||
self.control_point is not None and | ||
self.control_point == 'vol flow']): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if self.control_point in ['mass flow', 'vol flow']:
alicat/serial.py
Outdated
value = p_value | ||
command = '{addr}$$w21={v}\r'.format(addr=self.address, v=value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could save a line on this and just do:
command = '{addr}$$w21={v}\r'.format(addr=self.address, v=p_value)
Also applies to below functions.
@@ -14,8 +14,8 @@ | |||
class FlowMeter(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is on me to fix. Having to copy-paste the same code twice is super messy. Let's focus on getting the serial.py
file working and then I'll commit to putting time into refactoring so you don't need to repeat your code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. I really appreciate you putting in the time for refactoring. Also, I will admit that I do not have a setup for testing a TCP connection so only serial.py is thoroughly tested by me.
alicat/__init__.py
Outdated
@@ -19,7 +19,8 @@ def command_line(): | |||
"to read devices routed through a converter.") | |||
parser.add_argument('--address', '-a', default='A', type=str, help="The " | |||
"device address, A-D. Should only be used if multiple " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was there all along, but might as well fix it now. I believe they go A-Z not A-D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup it is A-Z! I changed it.
I have a box of controllers with all our main firmware except 4v - can't remember when that was "borrowed". Initially tested all the code for each of the firmware except that one, and then I've been testing the refactored version with our new 9v firmware. |
Thank you so much! I can't express enough how much I appreciate you taking the time to show me the pythonic way. I did go through and make all the changes you recommended, as well as test them on some of my devices. |
See #15. Thanks @marinapalese!
I made a few tweaks and uploaded to PyPI. @marinapalese - can you confirm it works by updating your driver ( @alexrudd2 - I deleted the TCP driver and added a note that we can re-add it in if needed. If we end up needing it, we'll want to follow #16 to separate out the transport (serial, TCP, etc) and protocol (the strings the Alicat expects) so we're not repeating ourselves. Thanks again! |
?!? The TCP driver was the only one I ever used! It was powering the pilot-lab. "Fortunately" we've now killed that controller and replaced it with |
Great! I left a comment in telling people to let us know if they're using the TCP driver. It's a couple hours of refactor if we want to add it back in right. Not a big deal but not a priority if it's going unused. |
@patrickfuller I checked it out and everything works with some small things.
|
@marinapalese thanks for reviewing! I updated the README to handle the first two points. Your third point is related to #14 and could be worth thinking through further. I'd recommend moving the conversation to that thread but here's a brief overview: Ideally, Also ideally, we'd read/write the registers directly for all functions. Not only does this open up more features (as you've already shown in this PR!) but it allows greater accuracy for getting/setting flow rates on more modern MFCs. This is a full library rewrite... but this is a small library so it's not out of the question. What would be needed is 1. a detailed list of all Alicat MFC versions and their communication protocols and 2. a good hardware test system with all supported MFCs that we could build a real test suite against. If it's something you're interested in leading or helping out with, let me know and I'd be happy to find a way to support. |
Several functions were added to the flowmeter and flowcontroller classes. The functions added include most serial override commands, and well as a way to create and write gas mixes to the Alicat (introduced in R22 5v firmware and onward).
set_gas()
was edited to accommodate numerical and string inputs. This will give access the full gas list, and is the best method to set a gas mix that has been created. I did not make any efforts add additional gases toself.gases
, since there's at least 60 that would need to be added and they are not often used._set_setpoint
in theFlowController
class, current was change fromfloat(line.split()[-2])
tofloat(line.split()[5])
. Checking from the back, again, has the possibility of incorrectly calling an error if there is a status code on the device. The setpoint will always be index 5 from the front on a controller, so there is no issue. This method was not employed inset_gas()
because if the device has a totalizer, the totalized flow will appear before the gas.