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

Add devantech dS378 ethernet relay agent #694

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 73 additions & 0 deletions docs/agents/devantech_dS378.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks for writing the documentation! One thing that could be added, since you wrote docstrings for the driver code, is a "Supporting APIs" section to autodoc the dS378 class in drivers.py.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.. highlight:: rst

.. _devantech_dS378:

========================
Devantech dS378 Agent
========================

This agent is designed to interface with devantech's dS378 ethernet relay.


.. argparse::
:filename: ../socs/agents/devantech_dS378/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
`````````````````````

An example site-config-file block::

{'agent-class': 'dS378Agent',
'instance-id': 'ds378',
'arguments': [['--port', 17123],
['--ip_address', '192.168.0.100']]
},


Docker Compose
``````````````

The dS378 Agent should be configured to run in a Docker container. An
example docker-compose service configuration is shown here::

ocs-ds378:
image: simonsobs/socs:latest
hostname: ocs-docker
network_mode: "host"
volumes:
- ${OCS_CONFIG_DIR}:/config:ro
environment:
- INSTANCE_ID=ds378
- LOGLEVEL=info

The ``LOGLEVEL`` environment variable can be used to set the log level for
debugging. The default level is "info".

Description
--------------

dS378 is a board with 8 relays that can be cotrolled via ethernet.
The relay can be used for both DC (up to 24 V) and AC (up to 250V).
The electronics box for the stimulator uses this board to control
shutter and powercycling devices such as motor controller or KR260 board.

The driver code assumes the board is configured to communicate with binary codes.
This configuration can be changed via web interface (but requires USB connection as well,
see documentation provided from the manufacturer).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add link to Manufacturer's documentation.

You can also configure the ip address and the port number with the same interface.

The device only accepts 10/100BASE communication.

Agent API
---------

.. autoclass:: socs.agents.devantech_dS378.agent.dS378Agent
:members:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ API Reference Full API documentation for core parts of the SOCS library.
agents/acu_agent
agents/bluefors_agent
agents/cryomech_cpa
agents/devantech_dS378
agents/fts_agent
agents/generator
agents/hi6200
Expand Down
Empty file.
208 changes: 208 additions & 0 deletions socs/agents/devantech_dS378/agent.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One general comment (also a bit of a nitpick, sorry) -- docstrings should always use """triple double quotes""", not single quotes. This is for consistency in the socs library, but also follows PEP8/PEP257.

Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
'''OCS agent for dS378 ethernet relay
'''
import argparse
import os
import time

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

from socs.agents.devantech_dS378.drivers import dS378

IP_DEFAULT = '192.168.215.241'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't hard code some default IP, that's going to depend on the network that the device is on.

PORT_DEFAULT = 17123

LOCK_RELEASE_SEC = 1.
LOCK_RELEASE_TIMEOUT = 10
ACQ_TIMEOUT = 100


class dS378Agent:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of a nitpick, but we're following PEP08 here, so would prefer DS378Agent (using the CapWords style) instead of taking the capitalization from the manufacturer.

'''OCS agent class for dS378 ethernet relay
'''

def __init__(self, agent, ip=IP_DEFAULT, port=17123):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make ip a required argument.

'''
Parameters
----------
ip : string
IP address
port : int
Port number
'''

Comment on lines +27 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move up to the class docstring, otherwise it won't render in Sphinx.

self.active = True
self.agent = agent
self.log = agent.log
self.lock = TimeoutLock()
self.take_data = False

self._dev = dS378(ip=ip, port=port)

self.initialized = False

agg_params = {'frame_length': 60}
self.agent.register_feed('relay',
record=True,
agg_params=agg_params,
buffer_time=1)

def start_acq(self, session, params):
'''Starts acquiring data.
'''
Comment on lines +53 to +54
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a standardized way to document the tasks and processes that is documented here: https://ocs.readthedocs.io/en/main/developer/agent_references/documentation.html#task-and-process-documentation

Important things to include are:

  • Method signature override -- this documents to the OCS Client author what the task/process names are. For this example, the first line should just be acq().
  • **Process** - leading the description.
  • "Notes" section that documents the session.data object, since this doesn't have a standard structure. Client authors will also rely on this documentation to know what keys/values are available in this object.

This comment applies to the rest of the tasks/processes in this agent, though "stop" methods don't need to follow this.

if params is None:
params = {}

f_sample = params.get('sampling_frequency', 0.5)
sleep_time = 1 / f_sample - 0.1
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a requirement, but we have something to help with doing this called the Pacemaker. See: https://ocs.readthedocs.io/en/main/api.html#ocs.ocs_twisted.Pacemaker

Feel free to use it or not.


with self.lock.acquire_timeout(timeout=0, job='acq') as acquired:
if not acquired:
self.log.warn(
f'Could not start acq because {self.lock.job} is already running')
return False, 'Could not acquire lock.'

session.set_status('running')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now gets set automatically within the Agent when the process starts. This should be removed here.


self.take_data = True
session.data = {"fields": {}}
last_release = time.time()

while self.take_data:
# Release lock
if time.time() - last_release > LOCK_RELEASE_SEC:
last_release = time.time()
if not self.lock.release_and_acquire(timeout=LOCK_RELEASE_TIMEOUT):
print(f'Re-acquire failed: {self.lock.job}')
return False, 'Could not re-acquire lock.'

# Data acquisition
current_time = time.time()
data = {'timestamp': current_time, 'block_name': 'relay', 'data': {}}

d_status = self._dev.get_status()
relay_list = self._dev.get_relays()
data['data']['V_sppl'] = d_status['V_sppl']
data['data']['T_int'] = d_status['T_int']
for i in range(8):
data['data'][f'Relay_{i + 1}'] = relay_list[i]

field_dict = {'relay': {'V_sppl': d_status['V_sppl'],
'T_int': d_status['T_int']}}
session.data['fields'].update(field_dict)

self.agent.publish_to_feed('relay', data)
session.data.update({'timestamp': current_time})

time.sleep(sleep_time)

self.agent.feeds['relay'].flush_buffer()

return True, 'Acquisition exited cleanly.'

def stop_acq(self, session, params=None):
"""
Stops the data acquisiton.
"""
Comment on lines +105 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the above comment about documentation. Since 'stop' within ocs is a command tied to the task/process, we will mark stop processes as private with a leading underscore, hiding them from being auto-documented in Sphinx. So stop_acq should become _stop_acq, and updated where used.

if self.take_data:
self.take_data = False
return True, 'requested to stop taking data.'

return False, 'acq is not currently running.'

def set_relay(self, session, params=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the @ocs_agent.param decorator to validate the parameters accepted here. That ensures no invalid parameters are passed to the task.

https://ocs.readthedocs.io/en/main/developer/agent_references/params.html#validation-using-param

'''Turns the relay on/off or pulses it

Parameters
----------
relay_number : int
relay_number, 1 -- 8
on_off : int or RelayStatus
1: on, 0: off
pulse_time : int, 32 bit
See document
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What document? Add link to this?

'''
if params is None:
params = {}

with self.lock.acquire_timeout(3, job='set_values') as acquired:
if not acquired:
self.log.warn('Could not start set_values because '
f'{self.lock.job} is already running')
return False, 'Could not acquire lock.'

if params.get('pulse_time') is None:
params['pulse_time'] = 0

self._dev.set_relay(relay_number=params['relay_number'],
on_off=params['on_off'],
pulse_time=params['pulse_time'])
Comment on lines +139 to +141
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current form this could possibly raise an AssertionError if invalid parameters are passed to the task. You should catch that, fail the task (return False), and log a message indicating the problem.

Using the param decorator commented on above would alleviate this problem, as you could ensure no invalid parameters are passed to the task.


return True, 'Set values'

def get_relays(self, session, params=None):
''' Get relay states'''
if params is None:
params = {}

with self.lock.acquire_timeout(3, job='get_relays') as acquired:
if not acquired:
self.log.warn('Could not start get_relays because '
f'{self.lock.job} is already running')
return False, 'Could not acquire lock.'

d_status = self._dev.get_relays()
session.data = {f'Relay_{i + 1}': d_status[i] for i in range(8)}

return True, 'Got relay status'


def make_parser(parser=None):
if parser is None:
parser = argparse.ArgumentParser()

pgroup = parser.add_argument_group('Agent Options')
pgroup.add_argument('--port', default=PORT_DEFAULT, type=int,
help='Port number for TCP communication.')
pgroup.add_argument('--ip_address',
help='IP address of the device.')

return parser


def main(args=None):
'''Boot OCS agent'''
txaio.start_logging(level=os.environ.get('LOGLEVEL', 'info'))

parser = make_parser()
args = site_config.parse_args(agent_class='dS378Agent',
parser=parser,
args=args)

agent_inst, runner = ocs_agent.init_site_agent(args)
ds_agent = dS378Agent(agent_inst, ip=args.ip_address, port=args.port)

agent_inst.register_task(
'set_relay',
ds_agent.set_relay
)

agent_inst.register_task(
'get_relays',
ds_agent.get_relays
)

agent_inst.register_process(
'acq',
ds_agent.start_acq,
ds_agent.stop_acq,
startup=True
)

runner.run(agent_inst, auto_reconnect=True)


if __name__ == '__main__':
main()
Loading