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

Agent for IFM KQ1001 continuous level sensor #762

Open
wants to merge 7 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
105 changes: 105 additions & 0 deletions docs/agents/ifm_kq1001_levelsensor.rst
Original file line number Diff line number Diff line change
@@ -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 <https://www.ifm.com/de/en/download/LR_Device>`_

Agent API
---------

.. autoclass:: socs.agents.ifm_kq1001_levelsensor.agent.LevelSensorAgent
:members:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Empty file.
188 changes: 188 additions & 0 deletions socs/agents/ifm_kq1001_levelsensor/agent.py
Original file line number Diff line number Diff line change
@@ -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(0.2, quantize=False)
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()
1 change: 1 addition & 0 deletions socs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down