Skip to content

Commit

Permalink
Adding Hi6200 Agent for reading LN2 on SATp (#555)
Browse files Browse the repository at this point in the history
* 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
3 people authored Feb 26, 2024
1 parent ccde1ba commit ee538a0
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 0 deletions.
87 changes: 87 additions & 0 deletions docs/agents/hi6200.rst
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:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ API Reference Full API documentation for core parts of the SOCS library.
agents/cryomech_cpa
agents/fts_agent
agents/generator
agents/hi6200
agents/hwp_encoder
agents/hwp_gripper
agents/hwp_pcu
Expand Down
159 changes: 159 additions & 0 deletions socs/agents/hi6200/agent.py
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()
82 changes: 82 additions & 0 deletions socs/agents/hi6200/drivers.py
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
1 change: 1 addition & 0 deletions socs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'FlowmeterAgent': {'module': 'socs.agents.ifm_sbn246_flowmeter.agent', 'entry_point': 'main'},
'FTSAerotechAgent': {'module': 'socs.agents.fts_aerotech.agent', 'entry_point': 'main'},
'GeneratorAgent': {'module': 'socs.agents.generator.agent', 'entry_point': 'main'},
'Hi6200Agent': {'module': 'socs.agents.hi6200.agent', 'entry_point': 'main'},
'HWPBBBAgent': {'module': 'socs.agents.hwp_encoder.agent', 'entry_point': 'main'},
'HWPGripperAgent': {'module': 'socs.agents.hwp_gripper.agent', 'entry_point': 'main'},
'HWPPCUAgent': {'module': 'socs.agents.hwp_pcu.agent', 'entry_point': 'main'},
Expand Down

0 comments on commit ee538a0

Please sign in to comment.