From 5031ea83e59093ff6e59553bcd97c7457d0bafd0 Mon Sep 17 00:00:00 2001 From: "Shawn W. Henderson" <20823858+swh76@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:54:39 -0700 Subject: [PATCH 1/6] Adding agent for aggregating data from IFM KQ1001 level sensor. --- .../agents/ifm_kq1001_levelsensor/__init__.py | 0 socs/agents/ifm_kq1001_levelsensor/agent.py | 188 ++++++++++++++++++ socs/plugin.py | 1 + 3 files changed, 189 insertions(+) create mode 100644 socs/agents/ifm_kq1001_levelsensor/__init__.py create mode 100644 socs/agents/ifm_kq1001_levelsensor/agent.py diff --git a/socs/agents/ifm_kq1001_levelsensor/__init__.py b/socs/agents/ifm_kq1001_levelsensor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/socs/agents/ifm_kq1001_levelsensor/agent.py b/socs/agents/ifm_kq1001_levelsensor/agent.py new file mode 100644 index 000000000..9bdb2085b --- /dev/null +++ b/socs/agents/ifm_kq1001_levelsensor/agent.py @@ -0,0 +1,188 @@ +import argparse +import time +from os import environ + +import requests +import txaio +from ocs import ocs_agent, site_config +from ocs.ocs_twisted import Pacemaker, TimeoutLock + + +def extract(value): + """ + Extract level and device status from raw hexidecimal value from + KQ1001 Process Data. + + Parameters + ---------- + value : str + Hexidecimal value from KQ1001 Process Data. + + Returns + ------- + float, int + The level in % and the device status. + + """ + binary = bin(int(value,16))[2:].zfill(32) + # Decode all of the process data fields, but most of them don't + # matter. + _b_pdv1 = binary[0:16] + _b_scale_levl = binary[16:24] + _b_device_status = binary[24:28] + _b_out3 = binary[29:30] + _b_out2 = binary[30:31] + _b_out1 = binary[31:32] + + pdv1 = int(_b_pdv1,2)*1.0 + device_status = int(_b_device_status,2) + + return pdv1,device_status + + +class LevelSensorAgent: + """ + Monitor the level sensor. + + Parameters + ---------- + agent : OCSAgent + OCSAgent object which forms this Agent. + ip_address: str + IP address of IO-Link master to make requests from. + daq_port: int + Port on IO-Link master that connects to level sensor. Choices are 1-4. + + """ + + def __init__(self, agent, ip_address, daq_port): + self.agent = agent + self.log = agent.log + self.lock = TimeoutLock() + + self.ip_address = ip_address + self.daq_port = daq_port + + # check make of the level sensor, otherwise data may make no + # sense + prod_adr = "/iolinkmaster/port[{}]/iolinkdevice/productname/getdata".format(self.daq_port) + q = requests.post('http://{}'.format(self.ip_address), json={"code": "request", "cid": -1, "adr": prod_adr}) + assert q.json()['data']['value'] == 'KQ1001', "Device is not an KQ1001 model level sensor. Give up!" + + self.take_data = False + + agg_params = {'frame_length': 60, + 'exclude_influx': False} + + # register the feed + self.agent.register_feed('levelsensor', + record=True, + agg_params=agg_params, + buffer_time=1 + ) + + @ocs_agent.param('test_mode', default=False, type=bool) + def acq(self, session, params=None): + """ + acq(test_mode=False) + + **Process** - Fetch values from the level sensor using the + IO-Link master. + + Parameters + ---------- + test_mode : bool, optional + 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. Note the units are [liters/min] and [Celsius]:: + + >>> response.session['data'] + {'timestamp': 1682630863.0066128, + 'fields': + {'level': 82.0, 'status': 0} + } + + """ + pm = Pacemaker(1, quantize=True) + self.take_data = True + + while self.take_data: + pm.sleep() + + dp = int(self.daq_port) + adr = "/iolinkmaster/port[{}]/iolinkdevice/pdin/getdata".format(dp) + url = 'http://{}'.format(self.ip_address) + + try: + r = requests.post(url, json={"code": "request", "cid": -1, "adr": adr}) + except requests.exceptions.ConnectionError as e: + self.log.warn(f"Connection error occured: {e}") + continue + + now = time.time() + value = r.json()['data']['value'] + + level_pct, status_int = extract(value) # units [gallons/minute], [F] + + data = {'block_name': 'levelsensor', + 'timestamp': now, + 'data': {'level': level_pct, + 'status': status_int} + } + + self.agent.publish_to_feed('levelsensor', data) + + session.data = {"timestamp": now, + "fields": {}} + + session.data['fields']['level'] = level_pct + session.data['fields']['status'] = status_int + + if params['test_mode']: + break + + 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_in=None): + if parser_in is None: + parser_in = argparse.ArgumentParser() + pgroup = parser_in.add_argument_group('Agent Options') + pgroup.add_argument("--ip-address", type=str, help="IP address of IO-Link master.") + pgroup.add_argument("--daq-port", type=int, help="Port on IO-Link master that level sensor is connected to.") + + return parser_in + + +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='LevelSensorAgent', parser=parser, args=args) + + agent, runner = ocs_agent.init_site_agent(args) + f = LevelSensorAgent(agent, args.ip_address, args.daq_port) + + agent.register_process('acq', f.acq, f._stop_acq, startup=True) + + runner.run(agent, auto_reconnect=True) + + +if __name__ == "__main__": + main() diff --git a/socs/plugin.py b/socs/plugin.py index 6ceec5d1b..2be5f18d4 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -6,6 +6,7 @@ 'CryomechCPAAgent': {'module': 'socs.agents.cryomech_cpa.agent', 'entry_point': 'main'}, 'FPGAAgent': {'module': 'socs.agents.holo_fpga.agent', 'entry_point': 'main'}, 'FlowmeterAgent': {'module': 'socs.agents.ifm_sbn246_flowmeter.agent', 'entry_point': 'main'}, + 'LevelSensorAgent': {'module': 'socs.agents.ifm_kq1001_levelsensor.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'}, From 4c927db0eb0368ebcc2090da746c7219ec7b3483 Mon Sep 17 00:00:00 2001 From: "Shawn W. Henderson" <20823858+swh76@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:43:50 -0700 Subject: [PATCH 2/6] First attempt at adding docs for new IFM KQ1001 agent. --- docs/agents/ifm_kq1001_levelsensor.rst | 105 +++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 106 insertions(+) create mode 100644 docs/agents/ifm_kq1001_levelsensor.rst diff --git a/docs/agents/ifm_kq1001_levelsensor.rst b/docs/agents/ifm_kq1001_levelsensor.rst new file mode 100644 index 000000000..16e001886 --- /dev/null +++ b/docs/agents/ifm_kq1001_levelsensor.rst @@ -0,0 +1,105 @@ +.. highlight:: rst + +.. _ifm_kq1001_levelsensor: + +========================== +IFM KQ1001 Level Sensor Agent +========================== + +The IFM KQ1001 Level Sensor Agent is an OCS Agent which monitors the +fluid level in percent reported by the sensor. The agent also records +the device status of the KQ1001 sensor. Monitoring is performed by +connecting the level sensor device to an IO-Link master device from +the same company; the querying of level sensor data is done via HTTP +requests to the IO-Link master. + +.. argparse:: + :filename: ../socs/agents/ifm_kq1001_levelsensor/agent.py + :func: add_agent_args + :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 IFM KQ1001 Level Sensor Agent we need to add a +LevelSensorAgent block to our ocs configuration file. Here is an +example configuration block using all of the available arguments:: + + {'agent-class': 'LevelSensorAgent', + 'instance-id': 'level', + 'arguments': [['--ip-address', '10.10.10.159'], + ['--daq-port', '2']]}, + +.. note:: + The ``--ip-address`` argument should use the IP address of the IO-Link + master. + +Docker Compose +`````````````` + +The IFM KQ1001 Level Sensor Agent should be configured to run in a +Docker container. An example docker compose service configuration is +shown here:: + + ocs-level: + image: simonsobs/socs:latest + hostname: ocs-docker + network_mode: "host" + volumes: + - ${OCS_CONFIG_DIR}:/config:ro + environment: + - INSTANCE_ID=level + - SITE_HUB=ws://127.0.0.1:8001/ws + - SITE_HTTP=http://127.0.0.1:8001/call + - LOGLEVEL=info + +The ``LOGLEVEL`` environment variable can be used to set the log level for +debugging. The default level is "info". + +Description +----------- + +The KQ1001 Level Sensor is a device from IFM Electronic, and can be +used to monitor the level of a process fluid (like water) in a +nonconductive tank through the tank wall. The sensor can be +noninvasively taped to the outside of the tank. The sensor must be +calibrated to the tank before it can report level readings. The +calibration is best if performed on the tank when it is empty or full, +but can also be performed if the tank is partially full. For more +information on the calibration, see the operating instructions for the +KQ1001, available on the IFM website. The Agent communicates with the +level sensor via an AL1340 IFM Electronic device--an IO-Link master +from which the agent makes HTTP requests. The level sensor plugs into +1 of 4 ports on the IO-Link master, and the agent queries data +directly from that IO-Link master port. This is only possible when an +ethernet connection is established via the IO-Link master's IoT port. + +IO-Link Master Network +``````````````````````` +Once plugged into the IoT port on your IO-Link master, the IP address of the +IO-Link master is automatically set by a DHCP server in the network. If no DHCP +server is reached, the IP address is automatically assigned to the factory setting +for the IoT port (169.254.X.X). + +IO-Link Visualization Software +``````````````````````````````` +A Windows software called LR Device exists for parameter setting and visualization +of IO-Link master and device data. The software download link is below should the +user need it for changing settings on the IO-Link master. On the LR Device software +panel, click the 'read from device' button on the upper right (leftmost IOLINK +button); the software will then search for the IO-Link master. Once found, it will +inform the user of the IO-Link master model number (AL1340) and its IP address. + + - `LR Device Software `_ + +Agent API +--------- + +.. autoclass:: socs.agents.ifm_kq1001_levelsensor.agent.LevelSensorAgent + :members: diff --git a/docs/index.rst b/docs/index.rst index 518ed4125..260812f63 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,6 +54,7 @@ API Reference Full API documentation for core parts of the SOCS library. agents/holo_fpga agents/holo_synth agents/ibootbar + agents/ifm_kq1001_levelsensor agents/ifm_sbn246_flowmeter agents/labjack agents/lakeshore240 From 0b6335560494eae8c00cbf2f21e848cde87909f8 Mon Sep 17 00:00:00 2001 From: "Shawn W. Henderson" <20823858+swh76@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:41:46 -0700 Subject: [PATCH 3/6] Reduce cadence of polling, disable Pacemaker quantize. Trivial typo in docs. --- docs/agents/ifm_kq1001_levelsensor.rst | 4 ++-- socs/agents/ifm_kq1001_levelsensor/agent.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/agents/ifm_kq1001_levelsensor.rst b/docs/agents/ifm_kq1001_levelsensor.rst index 16e001886..5e56244f9 100644 --- a/docs/agents/ifm_kq1001_levelsensor.rst +++ b/docs/agents/ifm_kq1001_levelsensor.rst @@ -2,9 +2,9 @@ .. _ifm_kq1001_levelsensor: -========================== +============================= IFM KQ1001 Level Sensor Agent -========================== +============================= The IFM KQ1001 Level Sensor Agent is an OCS Agent which monitors the fluid level in percent reported by the sensor. The agent also records diff --git a/socs/agents/ifm_kq1001_levelsensor/agent.py b/socs/agents/ifm_kq1001_levelsensor/agent.py index 9bdb2085b..421c35afe 100644 --- a/socs/agents/ifm_kq1001_levelsensor/agent.py +++ b/socs/agents/ifm_kq1001_levelsensor/agent.py @@ -107,7 +107,7 @@ def acq(self, session, params=None): } """ - pm = Pacemaker(1, quantize=True) + pm = Pacemaker(0.2, quantize=False) self.take_data = True while self.take_data: From a0f6fbf7594ce482c00e0c9ec3d9c657ef639cc2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:46:43 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/index.rst | 2 +- socs/agents/ifm_kq1001_levelsensor/agent.py | 10 +++++----- socs/plugin.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 260812f63..d07b5674f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,7 +54,7 @@ API Reference Full API documentation for core parts of the SOCS library. agents/holo_fpga agents/holo_synth agents/ibootbar - agents/ifm_kq1001_levelsensor + agents/ifm_kq1001_levelsensor agents/ifm_sbn246_flowmeter agents/labjack agents/lakeshore240 diff --git a/socs/agents/ifm_kq1001_levelsensor/agent.py b/socs/agents/ifm_kq1001_levelsensor/agent.py index 421c35afe..378f76f64 100644 --- a/socs/agents/ifm_kq1001_levelsensor/agent.py +++ b/socs/agents/ifm_kq1001_levelsensor/agent.py @@ -24,7 +24,7 @@ def extract(value): The level in % and the device status. """ - binary = bin(int(value,16))[2:].zfill(32) + binary = bin(int(value, 16))[2:].zfill(32) # Decode all of the process data fields, but most of them don't # matter. _b_pdv1 = binary[0:16] @@ -34,10 +34,10 @@ def extract(value): _b_out2 = binary[30:31] _b_out1 = binary[31:32] - pdv1 = int(_b_pdv1,2)*1.0 - device_status = int(_b_device_status,2) - - return pdv1,device_status + pdv1 = int(_b_pdv1, 2) * 1.0 + device_status = int(_b_device_status, 2) + + return pdv1, device_status class LevelSensorAgent: diff --git a/socs/plugin.py b/socs/plugin.py index 2be5f18d4..8925aafd9 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -6,7 +6,7 @@ 'CryomechCPAAgent': {'module': 'socs.agents.cryomech_cpa.agent', 'entry_point': 'main'}, 'FPGAAgent': {'module': 'socs.agents.holo_fpga.agent', 'entry_point': 'main'}, 'FlowmeterAgent': {'module': 'socs.agents.ifm_sbn246_flowmeter.agent', 'entry_point': 'main'}, - 'LevelSensorAgent': {'module': 'socs.agents.ifm_kq1001_levelsensor.agent', 'entry_point': 'main'}, + 'LevelSensorAgent': {'module': 'socs.agents.ifm_kq1001_levelsensor.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'}, From d9597c6cc2a419f69ae14c69130d6fa16fa8b1e6 Mon Sep 17 00:00:00 2001 From: "Shawn W. Henderson" <20823858+swh76@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:49:20 -0700 Subject: [PATCH 5/6] Flake8 fix. --- socs/agents/ifm_kq1001_levelsensor/agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/socs/agents/ifm_kq1001_levelsensor/agent.py b/socs/agents/ifm_kq1001_levelsensor/agent.py index 421c35afe..d7bd983b5 100644 --- a/socs/agents/ifm_kq1001_levelsensor/agent.py +++ b/socs/agents/ifm_kq1001_levelsensor/agent.py @@ -28,11 +28,11 @@ def extract(value): # Decode all of the process data fields, but most of them don't # matter. _b_pdv1 = binary[0:16] - _b_scale_levl = binary[16:24] + #_b_scale_levl = binary[16:24] _b_device_status = binary[24:28] - _b_out3 = binary[29:30] - _b_out2 = binary[30:31] - _b_out1 = binary[31:32] + #_b_out3 = binary[29:30] + #_b_out2 = binary[30:31] + #_b_out1 = binary[31:32] pdv1 = int(_b_pdv1,2)*1.0 device_status = int(_b_device_status,2) From 5385fe65ce5316dedae9f0a9b597c20c16e2b4ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:50:34 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- socs/agents/ifm_kq1001_levelsensor/agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/socs/agents/ifm_kq1001_levelsensor/agent.py b/socs/agents/ifm_kq1001_levelsensor/agent.py index 38c19db18..a76f4d431 100644 --- a/socs/agents/ifm_kq1001_levelsensor/agent.py +++ b/socs/agents/ifm_kq1001_levelsensor/agent.py @@ -28,11 +28,11 @@ def extract(value): # Decode all of the process data fields, but most of them don't # matter. _b_pdv1 = binary[0:16] - #_b_scale_levl = binary[16:24] + # _b_scale_levl = binary[16:24] _b_device_status = binary[24:28] - #_b_out3 = binary[29:30] - #_b_out2 = binary[30:31] - #_b_out1 = binary[31:32] + # _b_out3 = binary[29:30] + # _b_out2 = binary[30:31] + # _b_out1 = binary[31:32] pdv1 = int(_b_pdv1, 2) * 1.0 device_status = int(_b_device_status, 2)