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

Convert LS372 class to TCPInterface subclass #775

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
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 @@ -97,7 +97,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
64 changes: 11 additions & 53 deletions socs/Lakeshore/Lakeshore372.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Lakeshore372.py

import socket
import sys
import time

import numpy as np

from socs.tcp import TCPInterface

# Lookup keys for command parameters.
autorange_key = {'0': 'off',
'1': 'on',
Expand Down Expand Up @@ -173,38 +174,7 @@
heater_display_lock = {v: k for k, v in heater_display_key.items()}


def _establish_socket_connection(ip, timeout, port=7777):
"""Establish socket connection to the LS372.

Parameters
----------
ip : str
IP address of the LS372
timeout : int
timeout period for the socket connection in seconds
port : int
Port for the connection, defaults to the default LS372 port of 7777

Returns
-------
socket.socket
The socket object with open connection to the 372

"""
com = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
com.connect((ip, port))
except OSError as e:
if 'No route to host' in e.strerror:
raise ConnectionError("Cannot connect to LS372")
else:
raise e
com.settimeout(timeout)

return com


class LS372:
class LS372(TCPInterface):
"""
Lakeshore 372 class.

Expand All @@ -213,10 +183,12 @@ class LS372:
index 0 corresponding to the control channel, 'A'
"""

def __init__(self, ip, timeout=10, num_channels=16):
self.com = _establish_socket_connection(ip, timeout)
def __init__(self, ip_address, port=7777, timeout=10, num_channels=16):
self.num_channels = num_channels

# Setup the TCP Interface
super().__init__(ip_address, port, timeout)

self.id = self.get_id()
self.autoscan = self.get_autoscan()
# Enable all channels
Expand All @@ -237,11 +209,7 @@ def __init__(self, ip, timeout=10, num_channels=16):
self.still_heater = Heater(self, 2)

def msg(self, message):
"""Send message to the Lakeshore 372 over ethernet.

If we're asking for something from the Lakeshore (indicated by a ? in
the message string), then we will attempt to ask twice before giving up
due to potential communication timeouts.
"""Send message to the Lakeshore 372 over TCP.

Parameters
----------
Expand All @@ -257,20 +225,10 @@ def msg(self, message):
msg_str = f'{message}\r\n'.encode()

if '?' in message:
self.com.send(msg_str)
# Try once, if we timeout, try again. Usually gets around single event glitches.
for attempt in range(2):
try:
resp = str(self.com.recv(4096), 'utf-8').strip()
break
except socket.timeout:
print("Warning: Caught timeout waiting for response to '%s', trying again "
"before giving up" % message)
if attempt == 1:
raise RuntimeError('Query response to Lakeshore timed out after two '
'attempts. Check connection.')
self.send(msg_str)
resp = str(self.recv(), 'utf-8').strip()
else:
self.com.send(msg_str)
self.send(msg_str)
resp = ''

return resp
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