-
Notifications
You must be signed in to change notification settings - Fork 13
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
'''OCS agent class for dS378 ethernet relay | ||
''' | ||
|
||
def __init__(self, agent, ip=IP_DEFAULT, port=17123): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make |
||
''' | ||
Parameters | ||
---------- | ||
ip : string | ||
IP address | ||
port : int | ||
Port number | ||
''' | ||
|
||
Comment on lines
+27
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using the 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current form this could possibly raise an 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() |
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 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 indrivers.py
.