-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Hi6200 Agent for reading LN2 on SATp (#555)
* Adding Hi6200 Agent for reading LN2 on SATp * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Corrected some whitespace and style issues * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixed my fight with the pre-commit bot * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added in edits from Brian's PR comments. Added docs. (Attempted) removal of all swap files * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Attempting to fix disconnect error catching * Added catch in monitor weight for AttributeError. Cleaned up code as per Brian's comments. * Actually fixed Brian's comments regarding pacemaker, privatizing decoding function, etc. * Remove layer of nesting in main process * Add job names to lock * Wrap agent code to 80 characters * Add agent to docs index * Add driver code to docs page * Format driver docstrings and wrap code to 80 characters --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Brian Koopman <[email protected]>
- Loading branch information
1 parent
ccde1ba
commit ee538a0
Showing
5 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
.. highlight:: rst | ||
|
||
.. _Hi6200: | ||
|
||
============== | ||
Hi6200 Agent | ||
============== | ||
|
||
This agent uses Modbus TCP to communicate with the Hi6200 Weight Sensor. | ||
This agent uses ModbusClient from pyModbusTCP to facilitate the communication. | ||
The agent is able to communicate over ethernet to read and monitor the net and | ||
gross weights of the scale. | ||
|
||
.. argparse:: | ||
:filename: ../socs/agents/hi6200/agent.py | ||
:func: make_parser | ||
:prog: python3 agent.py | ||
|
||
|
||
Configuration File Examples | ||
--------------------------- | ||
Below are configuration examples for the ocs config file and for running the | ||
Agent in a docker container. | ||
|
||
OCS Site Config | ||
``````````````` | ||
|
||
To configure the Hi6200 Agent we need to add a block to our ocs | ||
configuration file. Here is an example configuration block using all of | ||
the available arguments:: | ||
|
||
{'agent-class': 'Hi6200Agent', | ||
'instance-id': 'hi6200', | ||
'arguments': [ | ||
['--ip-address', '192.168.11.43'], | ||
['--tcp-port', '502'] | ||
]}, | ||
|
||
The Hi6200 Agent requires the IP address and ModbusTCP port of the Hi6200 | ||
in order to connect to the Hi6200. The default ModbusTCP port on the Hi6200 | ||
is 502. | ||
|
||
Docker Compose | ||
`````````````` | ||
|
||
The SCPI PSU Agent should be configured to run in a Docker container. | ||
An example docker-compose service configuration is shown here:: | ||
|
||
ocs-hi6200: | ||
image: simonsobs/socs:latest | ||
hostname: ocs-docker | ||
network_mode: "host" | ||
environment: | ||
- INSTANCE_ID=hi6200 | ||
volumes: | ||
- ${OCS_CONFIG_DIR}:/config:ro | ||
|
||
Agent API | ||
--------- | ||
|
||
.. autoclass:: socs.agents.hi6200.agent.Hi6200Agent | ||
:members: | ||
|
||
Example Clients | ||
--------------- | ||
|
||
Below is an example client demonstrating full agent functionality.:: | ||
|
||
from ocs.ocs_client import OCSClient | ||
|
||
# Initialize the power supply | ||
scale = OCSClient('hi6200') | ||
scale.init.start() | ||
scale.init.wait() | ||
|
||
# Begin Monitoring Weight | ||
scale.monitor_weight.start() | ||
|
||
#Stop Monitoring Weight | ||
scale.stop_monitoring.start() | ||
|
||
Supporting APIs | ||
--------------- | ||
|
||
.. autoclass:: socs.agents.hi6200.drivers.Hi6200Interface | ||
:members: | ||
:noindex: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import argparse | ||
import time | ||
|
||
from ocs import ocs_agent, site_config | ||
from ocs.ocs_twisted import Pacemaker, TimeoutLock | ||
|
||
from socs.agents.hi6200.drivers import Hi6200Interface | ||
|
||
|
||
class Hi6200Agent: | ||
""" | ||
Agent to connect to the Hi6200 weight controller that measures the weight | ||
of the LN2 dewar on the SAT platform. | ||
Parameters: | ||
ip_address (string): IP address set on the Hi6200 | ||
tcp_port (int): Modbus TCP port of the Hi6200. | ||
Default set on the device is 502. | ||
scale (Hi6200Interface): A driver object that allows | ||
for communication with the scale. | ||
""" | ||
|
||
def __init__(self, agent, ip_address, tcp_port): | ||
self.agent = agent | ||
self.log = agent.log | ||
self.lock = TimeoutLock() | ||
|
||
self.ip_address = ip_address | ||
self.tcp_port = tcp_port | ||
self.scale = None | ||
|
||
self.monitor = False | ||
|
||
# Registers Scale Output | ||
agg_params = { | ||
'frame_length': 10 * 60, | ||
} | ||
self.agent.register_feed('scale_output', | ||
record=True, | ||
agg_params=agg_params, | ||
buffer_time=0) | ||
|
||
@ocs_agent.param('_') | ||
def init(self, session, params=None): | ||
"""init() | ||
**Task** - Initialize connection to the Hi 6200 Weight Sensor. | ||
""" | ||
with self.lock.acquire_timeout(0, job='init') as acquired: | ||
if not acquired: | ||
return False, "Could not acquire lock" | ||
|
||
self.scale = Hi6200Interface(self.ip_address, self.tcp_port) | ||
|
||
self.log.info("Connected to scale.") | ||
|
||
return True, 'Initialized Scale.' | ||
|
||
@ocs_agent.param('wait', type=float, default=1) | ||
def monitor_weight(self, session, params=None): | ||
"""monitor_weight(wait=1) | ||
**Process** - Continuously monitor scale gross and net weights. | ||
Parameters: | ||
wait (float, optional): Time to wait between measurements | ||
[seconds]. | ||
""" | ||
session.set_status('running') | ||
self.monitor = True | ||
|
||
pm = Pacemaker(1, quantize=True) | ||
while self.monitor: | ||
|
||
pm.sleep() | ||
with self.lock.acquire_timeout(1, job='monitor_weight') as acquired: | ||
if not acquired: | ||
self.log.warn("Could not start monitor_weight because " | ||
+ f"{self.lock.job} is already running") | ||
return False, "Could not acquire lock." | ||
|
||
data = { | ||
'timestamp': time.time(), | ||
'block_name': 'weight', | ||
'data': {} | ||
} | ||
|
||
try: | ||
# Grab the gross and net weights from the scale. | ||
gross_weight = self.scale.read_scale_gross_weight() | ||
net_weight = self.scale.read_scale_net_weight() | ||
|
||
# The above functions return None when an Attribute error | ||
# is thrown. If they did not return None and threw no | ||
# errors, the data is good. | ||
if (gross_weight is not None) and (net_weight is not None): | ||
data['data']["Gross"] = gross_weight | ||
data['data']["Net"] = net_weight | ||
self.agent.publish_to_feed('scale_output', data) | ||
|
||
# Occurs when the scale disconnects. | ||
except AttributeError as e: | ||
self.log.error("Connection with scale failed. Check that " | ||
+ f"the scale is connected: {e}") | ||
return False, "Monitoring weight failed" | ||
|
||
except ValueError as e: | ||
self.log.error("Scale responded with an anomolous number, " | ||
+ f"ignorning: {e}") | ||
|
||
except TypeError as e: | ||
self.log.error("Scale responded with 'None' and broke the " | ||
+ f"hex decoding, trying again: {e}") | ||
|
||
return True, "Finished monitoring weight" | ||
|
||
def stop_monitoring(self, session, params=None): | ||
self.monitor = False | ||
return True, "Stopping current monitor" | ||
|
||
|
||
def make_parser(parser=None): | ||
"""Build the argument parser for the Agent. Allows sphinx to automatically | ||
build documentation based on this function. | ||
""" | ||
if parser is None: | ||
parser = argparse.ArgumentParser() | ||
|
||
# Add options specific to this agent. | ||
pgroup = parser.add_argument_group('Agent Options') | ||
pgroup.add_argument('--ip-address') | ||
pgroup.add_argument('--tcp-port') | ||
|
||
return parser | ||
|
||
|
||
def main(args=None): | ||
|
||
parser = make_parser() | ||
args = site_config.parse_args(agent_class='Hi6200Agent', | ||
parser=parser, | ||
args=args) | ||
|
||
agent, runner = ocs_agent.init_site_agent(args) | ||
|
||
p = Hi6200Agent(agent, args.ip_address, int(args.tcp_port)) | ||
|
||
agent.register_task('init', p.init) | ||
|
||
agent.register_process('monitor_weight', p.monitor_weight, p.stop_monitoring) | ||
|
||
runner.run(agent, auto_reconnect=True) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import struct | ||
|
||
from pyModbusTCP.client import ModbusClient | ||
|
||
|
||
class Hi6200Interface(): | ||
""" | ||
Connects to the Hi6200 weight sensor using a TCP ModbusClient with pyModbusTCP. | ||
The Modbus Client uses a socket connection to facillitate Modbus communications. | ||
ModbusClient requires an IP address and port to connect. | ||
The Gross and Net weight sensors are always available to read on the 8,9 | ||
and 6,7 registers respectively. | ||
ModbusClient will not throw errors upon incorrect ip_address! | ||
ModbusClient auto-reconnects upon socket failure if auto_open is True. | ||
""" | ||
|
||
def __init__(self, ip_address, tcp_port, verbose=False, **kwargs): | ||
self.scale = ModbusClient(host=ip_address, | ||
port=tcp_port, | ||
auto_open=True, | ||
auto_close=False) | ||
|
||
def _decode_scale_weight_registers(self, register_a, register_b): | ||
""" | ||
Decodes the scales weight registers and returns a single weight value | ||
(float). | ||
The scale holds both the net and gross weights in permanent holding | ||
registers. Each weight is held across 2 registers in 4 hex bits (2 hex | ||
bits/4 bits per register, 8 bits total). The hex bits must be | ||
concatenated and converted to a float. | ||
""" | ||
# Strip the '0x' hex bit | ||
# We must have 8 total bits to convert, so we zfill until each register | ||
# value is 4 bits | ||
hex_a = hex(register_b)[2:].zfill(4) | ||
hex_b = hex(register_b)[2:].zfill(4) | ||
|
||
# Concatenate the hex bits in cdab order. | ||
hex_weight = hex_b + hex_a | ||
|
||
# This struct function converts the concatenated hex bits to a float. | ||
return struct.unpack('!f', bytes.fromhex(hex_weight))[0] | ||
|
||
def read_scale_gross_weight(self): | ||
""" | ||
Returns: | ||
float: The current gross weight reading of the scale in the sensors | ||
chosen unit (kg). | ||
""" | ||
try: | ||
# The gross weight is always available on the 8,9 registers. | ||
# Reading these registers will return an int. | ||
a, b = self.scale.read_holding_registers(8, 2) | ||
|
||
return self._decode_scale_weight_registers(a, b) | ||
|
||
except AttributeError: | ||
return None | ||
|
||
def read_scale_net_weight(self): | ||
""" | ||
Returns: | ||
float: The current net weight reading of the scale in the sensors | ||
chosen unit (kg). | ||
""" | ||
try: | ||
# The gross weight is always available on the 6,7 registers. | ||
# Reading these registers will return an int. | ||
a, b = self.scale.read_holding_registers(6, 2) | ||
|
||
return self._decode_scale_weight_registers(a, b) | ||
|
||
except AttributeError: | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters