Skip to content

Commit

Permalink
Merge pull request #769 from simonsobs/koopman/tcp-device-interface-c…
Browse files Browse the repository at this point in the history
…lass

Create `TCPInterface` class as base class for TCP based interfaces to devices
  • Loading branch information
BrianJKoopman authored Nov 21, 2024
2 parents 2847160 + 97d00a1 commit 97054e6
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 90 deletions.
1 change: 1 addition & 0 deletions docs/agents/cryomech_cpa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,4 @@ Supporting APIs

.. autoclass:: socs.agents.cryomech_cpa.agent.PTC
:members:
:show-inheritance:
9 changes: 9 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ socs.snmp
:undoc-members:
:show-inheritance:

socs.tcp
---------

.. automodule:: socs.tcp
:members:
:undoc-members:
:show-inheritance:
:noindex:

socs.testing
------------

Expand Down
File renamed without changes.
83 changes: 83 additions & 0 deletions docs/developer/interfaces/tcp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.. _tcp:

===================================
Transmission Control Protocol (TCP)
===================================

SOCS provides a standard interface for connecting to devices using TCP. This
page details how to use this interface. The primary benefit to using this
interface is the included error handling.

A few important things to know about the behavior of the interface class:

* The interface tries to connect to the device when instantiated.
* It will log but not raise an error if it cannot connect, instead
``self.comm`` will be ``None``.
* The connection will be reset when ``send()`` is called if this happens.
An exception will be raised if it still cannot connect.
* The interface is built to mimic ``socket.send()`` and ``socket.recv()``, but
uses ``socket.sendall()`` in its implementation, so all bytes in the included
message are sent to the socket.

See the example below for how to implement use of the ``TCPInterface`` class in
your device drivers and how to add error handling to the agent.

Example
-------
An example of using ``TCPInterface`` to create a class that interfaces with a
device::

from socs.tcp import TCPInterface

class Device(TCPInterface):
def __init__(self, ip_address, port=501, timeout=10, *args, **kwargs):
# Setup the TCP Interface
super().__init__(ip_address, port, timeout)

def get_data(self):
self.send(query_string)
data = self.recv()
# Optionally perform any decoding required
return data

Within the agent code where ``Device.get_data`` is used you should now handle
the possible ``ConnectionError``, as shown below.

.. note::
This example is stripped down to focus on the error handling. Important
parts of the agent process are missing here, like obtaining the lock and
publishing data to a feed.

.. code-block::
class DeviceAgent:
self.device = Device('192.168.1.2')
def main(self, session, params):
"""Main data acquisition process."""
while session.status in ['starting', 'running']:
try:
data = self.device.get_data()
if session.degraded:
self.log.info("Connection re-established.")
session.degraded = False
except ConnectionError:
self.log.error("Failed to get data from device. Check network connection.")
session.degraded = True
time.sleep(1) # wait between reconnection attempts
continue
return True, "Main process exited successfully."
See existing TCP agents, such as the Cryomech CPA Agent (which the above
example is based on) for more examples.

API
---

If you are developing an agent that connects to a device using TCP, the
``TCPInterface`` class is available for use and detailed here:

.. autoclass:: socs.tcp.TCPInterface
:members:
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ API Reference Full API documentation for core parts of the SOCS library.
:caption: Developer Guide
:maxdepth: 2

developer/snmp
developer/interfaces/snmp
developer/interfaces/tcp
developer/testing

.. toctree::
Expand Down
119 changes: 30 additions & 89 deletions socs/agents/cryomech_cpa/drivers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import random
import selectors
import socket
import struct

from socs.tcp import TCPInterface

STX = '\x02'
ADDR = '\x10'
CMD = '\x80'
Expand All @@ -15,97 +15,45 @@
ESC_ESC = '\x32'


class PTC:
class PTC(TCPInterface):
"""Interface class for connecting to the pulse tube compressor.
Parameters
----------
ip_address : str
IP address of the device.
port : int
Associated port for TCP communication. Default is 502.
timeout : float
Duration in seconds that operations wait before giving up. Default is
10 seconds.
fake_errors : bool
Flag that generates random fake errors if True. Does not generate
errors if False. Defaults to False.
Attributes
----------
comm : socket.socket
Socket object that forms the connection to the compressor.
"""

def __init__(self, ip_address, port=502, timeout=10, fake_errors=False):
self.fake_errors = fake_errors

self.model = None
self.serial = None
self.software_revision = None

self.ip_address = ip_address
self.port = int(port)
self.timeout = timeout
self.comm = self._connect((self.ip_address, self.port))

def _connect(self, address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
try:
sock.connect(address)
except TimeoutError:
print(f"Connection not established within {self.timeout}.")
return
except OSError as e:
print(f"Unable to connect. {e}")
return
except Exception as e:
print(f"Caught unexpected {type(e).__name__} while connecting:")
print(f" {e}")
return
return sock

def reset(self):
print("Resetting the connection to the compressor.")
self.comm = self._connect((self.ip_address, self.port))

def _write(self, msg):
if self.comm is None:
print("Connection not established. Unable to send command.")
self.reset()
return

try:
self.comm.sendall(msg)
return
except (BrokenPipeError, ConnectionResetError) as e:
print(f"Connection error: {e}")
self.reset()
except TimeoutError as e:
print(f"Timeout error while writing: {e}")
self.reset()
except Exception as e:
print(f"Caught unexpected {type(e).__name__} during write:")
print(f" {e}")
self.reset()

# Try a second time before giving up
try:
self.comm.sendall(msg)
except (BrokenPipeError, ConnectionResetError) as e:
print(f"Connection error: {e}")
raise ConnectionError
except TimeoutError as e:
print(f"Timeout error while writing: {e}")
raise ConnectionError
except AttributeError:
raise ConnectionError("Unable to reset connection.")
except Exception as e:
print(f"Caught unexpected {type(e).__name__} during write:")
print(f" {e}")
raise ConnectionError

def _check_ready(self):
"""Check socket is ready to read from."""
if self.comm is None:
raise ConnectionError("Connection not established, not ready to read.")

sel = selectors.DefaultSelector()
sel.register(self.comm, selectors.EVENT_READ)
if not sel.select(self.timeout):
raise ConnectionError

def _read(self):
self._check_ready()
data = self.comm.recv(1024)
return data
# Setup the TCP Interface
super().__init__(ip_address, port, timeout)

def get_data(self):
"""
Gets the raw data from the ptc and returns it in a usable format.
Gets the raw data from the PTC and returns it in a usable format.
"""
self._write(self.buildRegistersQuery())
data = self._read()
self.send(self.buildRegistersQuery())
data = self.recv(1024)
data_flag, brd = self.breakdownReplyData(data)

return data_flag, brd
Expand Down Expand Up @@ -246,10 +194,3 @@ def breakdownReplyData(self, rawdata):
f"Skipping this data block. Bad output string is {rawdata}")

return data_flag, data

def __del__(self):
"""
If the PTC class instance is destroyed, close the connection to the
ptc.
"""
self.comm.close()
Loading

0 comments on commit 97054e6

Please sign in to comment.