diff --git a/docs/agents/ucsc_radiometer.rst b/docs/agents/ucsc_radiometer.rst new file mode 100644 index 000000000..6ae3e4329 --- /dev/null +++ b/docs/agents/ucsc_radiometer.rst @@ -0,0 +1,72 @@ +.. highlight:: rst + +.. _ucsc_radiometer: + +===================== +UCSC Radiometer Agent +===================== + +The UCSC Radiometer Agent monitors the PWV through the UCSC Radiometer web server. + +.. argparse:: + :filename: ../socs/agents/ucsc_radiometer/agent.py + :func: add_agent_args + :prog: python3 agent.py + +Dependencies +------------ + +The UCSC Radiometer Agent requires the `UCSC Radiometer Server +`_. This server runs where +the radiometer data files are written and makes readings available to the Agent +over HTTP. + +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 UCSC Radiometer Agent we need to a UCSCRadiometerAgent +block to our ocs configuration file. Here is an example configuration block +using all of the available arguments:: + + {'agent-class': 'UCSCRadiometerAgent', + 'instance-id': 'pwvs', + 'arguments':[['--url', 'http://127.0.0.1:5000']]}, + +.. note:: + The ``--url`` argument should be the address of the Flask server on the + Web which is publishing pwv data from a server connected to the + radiometer on-site. + +Docker Compose +`````````````` + +The UCSC Radiometer Agent should be configured to run in a Docker container. An +example docker-compose service configuration is shown here:: + + ocs-ucsc-radiometer: + image: simonsobs/socs:latest + hostname: ocs-docker + network_mode: host + volumes: + - ${OCS_CONFIG_DIR}:/config + environment: + - INSTANCE_ID=pwvs + +Description +----------- + +The UCSC radiometer measures precipitable water vapor (pwv) of the atmosphere, +and outputs the values to disk on a computer at the site where OCS +is not setup. A web server makes the PWV values available over HTTP. The Agent requests the PWV data and then publishes it to OCS. + +Agent API +--------- + +.. autoclass:: socs.agents.ucsc_radiometer.agent.UCSCRadiometerAgent + :members: diff --git a/docs/index.rst b/docs/index.rst index c61b6f35a..aacb3e38f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -71,6 +71,7 @@ API Reference Full API documentation for core parts of the SOCS library. agents/synacc agents/tektronix3021c agents/thorlabs_mc2000b + agents/ucsc_radiometer agents/ups agents/vantage_pro2 agents/wiregrid_actuator diff --git a/socs/agents/ocs_plugin_so.py b/socs/agents/ocs_plugin_so.py index 9fac352f2..f23a1fd31 100644 --- a/socs/agents/ocs_plugin_so.py +++ b/socs/agents/ocs_plugin_so.py @@ -45,6 +45,7 @@ ('SynthAgent', 'holo_synth/agent.py'), ('TektronixAWGAgent', 'tektronix3021c/agent.py'), ('ThorlabsMC2000BAgent', 'thorlabs_mc2000b/agent.py'), + ('UCSCRadiometerAgent', 'ucsc_radiometer/agent.py'), ('UPSAgent', 'ups/agent.py'), ('VantagePro2Agent', 'vantagepro2/agent.py'), ('WiregridActuatorAgent', 'wiregrid_actuator/agent.py'), diff --git a/socs/agents/ucsc_radiometer/__init__.py b/socs/agents/ucsc_radiometer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/socs/agents/ucsc_radiometer/agent.py b/socs/agents/ucsc_radiometer/agent.py new file mode 100644 index 000000000..7fdba1746 --- /dev/null +++ b/socs/agents/ucsc_radiometer/agent.py @@ -0,0 +1,142 @@ +import argparse +from os import environ + +import requests +import txaio +from ocs import ocs_agent, site_config +from ocs.ocs_twisted import Pacemaker, TimeoutLock + + +class UCSCRadiometerAgent: + """Monitor the PWV Flask Server. + + Parameters + ---------- + agent : OCS Agent + OCSAgent object which forms this Agent + url : str + url of the radiometer web server on the internet + + """ + + def __init__(self, agent, url): + self.agent = agent + self.log = agent.log + self.lock = TimeoutLock() + + self.url = url + + self.take_data = False + + agg_params = {'frame_length': 60, + 'exclude_influx': False} + + # register the feed + self.agent.register_feed('pwvs', + record=True, + agg_params=agg_params, + buffer_time=1 + ) + + self.last_published_reading = None + + @ocs_agent.param('test_mode', default=False, type=bool) + def acq(self, session, params=None): + """acq() + + **Process** - Fetch values from PWV Flask Server + + Parameters + ---------- + test_mode : bool, option + Run the Process loop only once. Meant only for testing. + Default is False. + + Notes + ----- + The most recent data collected is stored in session data in the + following structure:: + + >>> response.session['data'] + {'timestamp': 1678820419.0, + 'pwv': 0.49253026985972237} + + """ + pm = Pacemaker(1 / 60, quantize=False) + + self.take_data = True + while self.take_data: + r = requests.get(self.url) + data = r.json() + last_pwv = data['pwv'] + last_timestamp = data['timestamp'] + + pwvs = {'block_name': 'pwvs', + 'timestamp': last_timestamp, + 'data': {'pwv': last_pwv} + } + + if self.last_published_reading is not None: + if last_timestamp > self.last_published_reading[1]: + self.agent.publish_to_feed('pwvs', pwvs) + self.last_published_reading = (last_pwv, last_timestamp) + else: + self.agent.publish_to_feed('pwvs', pwvs) + self.last_published_reading = (last_pwv, last_timestamp) + + session.data = {"timestamp": last_timestamp, + "pwv": {}} + + session.data['pwv'] = last_pwv + + if params['test_mode']: + break + else: + pm.sleep() + + return True, 'Acquisition exited cleanly.' + + def _stop_acq(self, session, params=None): + """ + Stops acq process. + """ + self.take_data = False + return True, 'Stopping acq process' + + +def add_agent_args(parser=None): + if parser is None: + parser = argparse.ArgumentParser() + pgroup = parser.add_argument_group('Agent Options') + pgroup.add_argument("--url", type=str, help="url for radiometer web server") + pgroup.add_argument("--test-mode", action='store_true', + help="Determines whether agent runs in test mode." + "Default is False.") + return parser + + +def main(args=None): + # For logging + txaio.use_twisted() + txaio.make_logger() + + txaio.start_logging(level=environ.get("LOGLEVEL", "info")) + + parser = add_agent_args() + args = site_config.parse_args(agent_class='UCSCRadiometerAgent', parser=parser, args=args) + + # test params + test_params = {'test_mode': False} + if args.test_mode: + test_params = {'test_mode': True} + + agent, runner = ocs_agent.init_site_agent(args) + pwv_agent = UCSCRadiometerAgent(agent, args.url) + + agent.register_process('acq', pwv_agent.acq, pwv_agent._stop_acq, startup=test_params) + + runner.run(agent, auto_reconnect=True) + + +if __name__ == "__main__": + main() diff --git a/socs/plugin.py b/socs/plugin.py index 3a6892f26..9e453f57e 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -36,6 +36,7 @@ 'SynthAgent': {'module': 'socs.agents.holo_synth.agent', 'entry_point': 'main'}, 'TektronixAWGAgent': {'module': 'socs.agents.tektronix3021c.agent', 'entry_point': 'main'}, 'ThorlabsMC2000BAgent': {'module': 'socs.agents.thorlabs_mc2000b.agent', 'entry_point': 'main'}, + 'UCSCRadiometerAgent': {'module': 'socs.agents.ucsc_radiometer.agent', 'entry_point': 'main'}, 'UPSAgent': {'module': 'socs.agents.ups.agent', 'entry_point': 'main'}, 'VantagePro2Agent': {'module': 'socs.agents.vantagepro2.agent', 'entry_point': 'main'}, 'WiregridActuatorAgent': {'module': 'socs.agents.wiregrid_actuator.agent', 'entry_point': 'main'}, diff --git a/tests/default.yaml b/tests/default.yaml index 086941f5d..de7cace0f 100644 --- a/tests/default.yaml +++ b/tests/default.yaml @@ -108,5 +108,10 @@ hosts: ['--test-mode'] ] }, + {'agent-class': 'UCSCRadiometerAgent', + 'instance-id': 'pwvs', + 'arguments': ['--url', 'http://127.0.0.1:5000', + '--test-mode'] + }, ] } diff --git a/tests/integration/test_ucsc_radiometer_agent_integration.py b/tests/integration/test_ucsc_radiometer_agent_integration.py new file mode 100644 index 000000000..a6b70cb23 --- /dev/null +++ b/tests/integration/test_ucsc_radiometer_agent_integration.py @@ -0,0 +1,47 @@ +import time +from datetime import datetime + +import ocs +import pytest +from flask import jsonify, request +from http_server_mock import HttpServerMock +from integration.util import docker_compose_file # noqa: F401 +from integration.util import create_crossbar_fixture +from ocs.base import OpCode +from ocs.testing import create_agent_runner_fixture, create_client_fixture + +wait_for_crossbar = create_crossbar_fixture() +run_agent = create_agent_runner_fixture( + "../socs/agents/ucsc_radiometer/agent.py", + "radiometer", + args=["--log-dir", "./logs/"], +) +client = create_client_fixture("pwvs") + + +@pytest.fixture +def http_mock(): + app = HttpServerMock(__name__) + + @app.route("/", methods=["GET"]) + def route_fn(): + if request.method == "GET": + time_now = datetime.now() + timestamp = time.mktime(time_now.timetuple()) + data = {'pwv': 1.2, 'timestamp': timestamp} + return jsonify(data) + else: + assert False, "Bad query" + + return app.run("127.0.0.1", 5000) + + +@pytest.mark.integtest +def test_ucsc_radiometer_acq(wait_for_crossbar, http_mock, run_agent, client): + with http_mock: + resp = client.acq.start(test_mode=True) + resp = client.acq.wait() + print(resp) + assert resp.status == ocs.OK + print(resp.session) + assert resp.session['op_code'] == OpCode.SUCCEEDED.value