Skip to content
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

Adding Hi6200 Agent for reading LN2 on SATp #555

Merged
merged 19 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/agents/hi6200.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
.. 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
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
: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:
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
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', args=[])
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
scale.init.start()
scale.init.wait()

# Begin Monitoring Weight
scale.monitor_weight.start()

#Stop Monitoring Weight
scale.stop_monitoring.start()
Binary file removed socs/agents/hi6200/.driver.py.swo
mjrand marked this conversation as resolved.
Outdated
Show resolved Hide resolved
Binary file not shown.
Binary file removed socs/agents/hi6200/.drivers.py.swm
Binary file not shown.
Binary file removed socs/agents/hi6200/.drivers.py.swn
Binary file not shown.
Binary file removed socs/agents/hi6200/.drivers.py.swo
Binary file not shown.
37 changes: 23 additions & 14 deletions socs/agents/hi6200/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,34 @@

from ocs import ocs_agent, site_config
from ocs.ocs_twisted import TimeoutLock
from ocs.ocs_twisted import Pacemaker

from socs.agents.hi6200.drivers import Hi6200Interface


class Hi6200Agent:
mjrand marked this conversation as resolved.
Show resolved Hide resolved
"""
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
Default set on the device is 502.
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
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.job = None
self.ip_address = ip_address
self.tcp_port = tcp_port
self.monitor = False

self.scale = None

self.monitor = False

# Registers Scale Output
agg_params = {
Expand Down Expand Up @@ -48,7 +60,7 @@ def init(self, session, params=None):

@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.

Expand All @@ -59,8 +71,11 @@ def monitor_weight(self, session, params=None):
"""
mjrand marked this conversation as resolved.
Show resolved Hide resolved
session.set_status('running')
self.monitor = True


pm = Pacemaker(1, quantize=True)
while self.monitor:

pm.sleep()
with self.lock.acquire_timeout(1) as acquired:
if acquired:
data = {
Expand All @@ -75,17 +90,11 @@ def monitor_weight(self, session, params=None):

self.agent.publish_to_feed('scale_output', data)

# Allow this process to be queried to return current data
session.data = data

except ValueError as e:
self.log.error(f"Scale responded with an anomolous number, ignorning: {e}")

except AttributeError as e:
self.log.error(f"Scale dropped TCP connection momentarily, trying again: {e}")

# Allow this process to be queried to return current data
session.data = data

except TypeError as e:
self.log.error(f"Scale responded with 'None' and broke the hex decoding, trying again: {e}")

else:
self.log.warn("Could not acquire in monitor_weight")
Expand Down
64 changes: 27 additions & 37 deletions socs/agents/hi6200/drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,36 @@ class Hi6200Interface():

def __init__(self, ip_address, tcp_port, verbose=False, **kwargs):
"""
Connects to the Hi6200 weight sensor using a TCP Modbus Client with pyModbusTCP.
This works ~similarly to a socket connection using a IP and port.
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.

ModbusClient will not throw errors upon incorrect ip_address!
ModbusClient will also allow multiple recconects unlike a socket.

ModbusClient auto-reconnects upon socket failure if auto_open is True.
"""

self.scale = ModbusClient(host=ip_address, port=tcp_port, auto_open=True, auto_close=False)

def decode_scale_weight_registers(register_a, register_b):
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
"""
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 the current gross weight reading of the scale in the sensors chosen unit (kg)
Expand All @@ -29,23 +51,7 @@ def read_scale_gross_weight(self):
# Reading these registers will return an int.
a, b = self.scale.read_holding_registers(8, 2)

# The ints read on the registers must be converted to hex.
# The hex bits are then concatenated and converted to float as CDAB

# Strip the '0x' hex bit prefix as it is not useful.
# Then concatenate the bits
hex_a = hex(a)[2:]
while len(hex_a) < 4:
hex_a = '0' + hex_a

hex_b = hex(b)[2:]
while len(hex_b) < 4:
hex_b = '0' + hex_b

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]
return decode_scale_weight_registers(a, b)
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved

def read_scale_net_weight(self):
"""
Expand All @@ -56,20 +62,4 @@ def read_scale_net_weight(self):
# Reading these registers will return an int.
a, b = self.scale.read_holding_registers(6, 2)

# The ints read on the registers must be converted to hex.
# The hex bits are then concatenated and converted to float as CDAB.

# Strip the '0x' hex bit prefix as it is not useful.
# Then concatenate the bits
hex_a = hex(a)[2:]
while len(hex_a) < 4:
hex_a = '0' + hex_a

hex_b = hex(b)[2:]
while len(hex_b) < 4:
hex_b = '0' + hex_b

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]
return decode_scale_weight_registers(a, b)
Loading