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

Adding support for direct Ethernet SCPI PSU Agent #715

Merged
merged 29 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4aa67fe
Add support for direct ethernet connection to Keithley PSUs, in addit…
Jun 7, 2024
b23a1b9
Installing pre-commit and fixing all files with 'pre-commit run --all…
Jun 10, 2024
833e1e4
Merge branch 'main' into UTAustin to keep UTAustin up-to-date with si…
Jun 12, 2024
bb99d54
Moving all direct-ethernet-connection changes from prologix_interface…
Jun 12, 2024
8125cc1
Fixing issues with parsing output from Keithley 2280S-60-3; fixing
Jun 13, 2024
dc08af0
Adding '--mode' : 'acq' to the ScpiPsuAgent to enable automatic
Jun 13, 2024
3d7d9d6
Merge branch 'simonsobs:main' into UTAustin
CAWNoodle Jul 2, 2024
18450cd
fixing issue with psu agent container crashing when attempting to mea…
Jul 24, 2024
fbbe937
Fixing issue related to Setuptools v71 issue #4483; fixing issue where
Jul 25, 2024
e37ef87
Fixing issue with data feed output corrupting influxdb when acq mode …
Jul 31, 2024
53d45bb
Periodic update of UTAustin branch with upstream changes from SO; Mer…
Jul 31, 2024
b2058d6
Adding PSU agent tasks for getting the output state, voltage
Aug 2, 2024
2429715
Merging upstream changes before pushing local changes.
Aug 5, 2024
4617ee6
Merging periodic updates from upstream simonsobs/socs repository
Aug 14, 2024
fa297e7
Making initial round of changes based on responses to PR #715
Aug 25, 2024
b42e7a1
Adding comments to explain the existence of the error code definitions
Aug 25, 2024
46b97c5
Changing output of get_output() to match the other get* functions.
Sep 10, 2024
5fff9a5
Merge branch 'main' into UTAustin
CAWNoodle Sep 10, 2024
1e0b56a
Fixing issue with call to _initialize_module() in init timing out due to
Sep 10, 2024
4d14a27
Removing the call to super().__init__(**kwargs) from the
Sep 14, 2024
de2911c
Start PSU tests in 'init' mode
BrianJKoopman Sep 16, 2024
1f1d250
Add expected response to channel enabled query in test
BrianJKoopman Sep 16, 2024
b603524
Add ability to set log level via env var
BrianJKoopman Sep 16, 2024
c92b751
Abort monitor process after test
BrianJKoopman Sep 16, 2024
6ec40a6
Remove reundant session message
BrianJKoopman Sep 16, 2024
e15cedf
Add test for initializing in 'acq' mode
BrianJKoopman Sep 16, 2024
0d13b3d
Remove commented initialization loop
BrianJKoopman Sep 16, 2024
1e4467f
Add num_channels to PsuInterface
BrianJKoopman Sep 16, 2024
869810b
Merge pull request #3 from simonsobs/koopman/UTAustin
CAWNoodle Sep 16, 2024
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
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ pyModbusTCP

# testing
-r requirements/testing.txt

# Contributing
pre-commit
209 changes: 198 additions & 11 deletions socs/agents/scpi_psu/agent.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import argparse
import os
import socket
import time
from typing import Optional

import txaio
from ocs import ocs_agent, site_config
from ocs.ocs_twisted import TimeoutLock

from socs.agents.scpi_psu.drivers import PsuInterface
from socs.agents.scpi_psu.drivers import PsuInterface, ScpiPsuInterface

# For logging
txaio.use_twisted()


class ScpiPsuAgent:
def __init__(self, agent, ip_address, gpib_slot):
def __init__(self, agent, ip_address, gpib_slot=None, port=None):
self.agent = agent
self.log = agent.log
self.lock = TimeoutLock()

self.job = None
self.ip_address = ip_address
self.gpib_slot = gpib_slot
self.gpib_slot = None
self.port = None
self.monitor = False

self.psu: Optional[ScpiPsuAgent] = None

if gpib_slot is not None:
self.gpib_slot = int(gpib_slot)
if port is not None:
self.port = int(port)

# Registers Temperature and Voltage feeds
agg_params = {
'frame_length': 10 * 60,
Expand All @@ -31,7 +42,7 @@ def __init__(self, agent, ip_address, gpib_slot):
agg_params=agg_params,
buffer_time=0)

@ocs_agent.param('_')
@ocs_agent.param('auto_acquire', default=False, type=bool)
def init(self, session, params=None):
"""init()

Expand All @@ -42,14 +53,51 @@ def init(self, session, params=None):
if not acquired:
return False, "Could not acquire lock"

while not self._initialize_module():
time.sleep(5)
if self.gpib_slot is None and self.port is None:
self.log.error('Either --gpib-slot or --port must be specified')
return False, "Parameter not set"
elif self.port is None: # Use the old Prologix-based GPIB code
try:
self.psu = PsuInterface(self.ip_address, self.gpib_slot)
self.idn = self.psu.identify()
except socket.timeout as e:
self.log.error(f"PSU timed out during connect: {e}")
return False, "Timeout"
else: # Use the new direct ethernet connection code
try:
self.psu = ScpiPsuInterface(self.ip_address, port=self.port)
self.idn = self.psu.identify()
except socket.timeout as e:
self.log.error(f"PSU timed out during connect: {e}")
return False, "Timeout"
except ValueError as e:
if (e.args[0].startswith('Model number')):
self.log.warn(f"PSU initialization: {e}. \
Number of channels defaults to 3. \
Suggest appending {e.args[-1]} to the list \
of known model numbers in scpi_psu/drivers.py")
else:
self.log.error(f"PSU initialization resulted in unknown ValueError: {e}")
return False, "ValueError"

self.log.info("Connected to psu: {}".format(self.idn))

auto_acquire = params.get('auto_acquire', False)

if auto_acquire:
acq_params = None
if self.psu.num_channels != 0:
acq_params = {'channels': [i + 1 for i in range(self.psu.num_channels)]}
self.agent.start('monitor_output', acq_params)
return True, 'Initialized PSU.'

def _initialize_module(self):
"""Initialize the ScpiPsu module."""
try:
self.psu = PsuInterface(self.ip_address, self.gpib_slot)
if self.port is None:
self.psu = PsuInterface(self.ip_address, self.gpib_slot)
else:
self.psu = ScpiPsuInterface(self.ip_address, port=self.port)
except (socket.timeout, OSError) as e:
self.log.warn(f"Error establishing connection: {e}")
self.psu = None
Expand Down Expand Up @@ -99,8 +147,13 @@ def monitor_output(self, session, params=None):

try:
for chan in params['channels']:
data['data']["Voltage_{}".format(chan)] = self.psu.get_volt(chan)
data['data']["Current_{}".format(chan)] = self.psu.get_curr(chan)
if self.psu.get_output(chan):
data['data']["Voltage_{}".format(chan)] = self.psu.get_volt(chan)
data['data']["Current_{}".format(chan)] = self.psu.get_curr(chan)
else:
self.log.warn("Cannot measure output when output is disabled")
self.monitor = False
return False, "Cannot measure output when output is disabled"
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
except socket.timeout as e:
self.log.warn(f"TimeoutError: {e}")
self.log.info("Attempting to reconnect")
Expand All @@ -121,6 +174,46 @@ def stop_monitoring(self, session, params=None):
self.monitor = False
return True, "Stopping current monitor"

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
def get_voltage(self, session, params=None):
"""get_voltage(channel)

**Task** - Measure and return the voltage of the power supply.

Parameters:
channel (int): Channel number (1, 2, or 3).

Examples:
Example for calling in a client::

client.get_voltage(channel=1)

Notes:
An example of the session data::

>>> response.session['data']
{'timestamp': 1723671503.4899583,
'channel': 1,
'voltage': 0.0512836}

"""
chan = params['channel']
with self.lock.acquire_timeout(1) as acquired:
if acquired:
if self.psu.get_output(chan):
data = {
'timestamp': time.time(),
'channel': chan,
'voltage': self.psu.get_volt(chan)
}

session.data = data
else:
return False, "Cannot measure output when output is disabled."
else:
return False, "Could not acquire lock"
return True, 'Channel {} voltage measured'.format(chan)

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
@ocs_agent.param('volts', type=float, check=lambda x: 0 <= x <= 30)
def set_voltage(self, session, params=None):
Expand All @@ -142,6 +235,46 @@ def set_voltage(self, session, params=None):

return True, 'Set channel {} voltage to {}'.format(params['channel'], params['volts'])

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
def get_current(self, session, params=None):
"""get_current(channel)

**Task** - Measure and return the current of the power supply.

Parameters:
channel (int): Channel number (1, 2, or 3).

Examples:
Example for calling in a client::

client.get_current(channel=1)

Notes:
An example of the session data::

>>> response.session['data']
{'timestamp': 1723671503.4899583,
'channel' : 1,
'current': 0.0103236}

"""
chan = params['channel']
with self.lock.acquire_timeout(1) as acquired:
if acquired:
# Check if output is enabled before measuring
if self.psu.get_output(chan):
data = {
'timestamp': time.time(),
'channel': chan,
'current': self.psu.get_curr(chan)
}
session.data = data
else:
return False, "Cannot measure output when output is disabled."
else:
return False, "Could not acquire lock"
return True, 'Channel {} current measured'.format(chan)

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
@ocs_agent.param('current', type=float)
def set_current(self, session, params=None):
Expand All @@ -162,6 +295,47 @@ def set_current(self, session, params=None):

return True, 'Set channel {} current to {}'.format(params['channel'], params['current'])

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
def get_output(self, session, params=None):
"""get_output(channel)

**Task** - Check if channel ouput is enabled or disabled.

Parameters:
channel (int): Channel number (1, 2, or 3).

Examples:
Example for calling in a client::

client.get_output(channel=1)

Notes:
An example of the session data::

>>> response.session['data']
{'timestamp': 1723671503.4899583,
'channel': 1,
'state': 1}

"""
chan = params['channel']
enabled = False
data = {}
with self.lock.acquire_timeout(1) as acquired:
if acquired:
data = {
'timestamp': time.time(),
'channel': chan,
'state': self.psu.get_output(chan)
}
session.data = data
else:
return False, "Could not acquire lock."
if enabled:
return True, 'Channel {} output is currently enabled.'.format(chan)
else:
return True, 'Channel {} output is currently disabled.'.format(chan)

@ocs_agent.param('channel', type=int, choices=[1, 2, 3])
@ocs_agent.param('state', type=bool)
def set_output(self, session, params=None):
Expand Down Expand Up @@ -195,25 +369,38 @@ def make_parser(parser=None):
pgroup = parser.add_argument_group('Agent Options')
pgroup.add_argument('--ip-address')
pgroup.add_argument('--gpib-slot')
pgroup.add_argument('--port')
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
pgroup.add_argument('--mode', type=str, default='acq',
choices=['init', 'acq'])

return parser


def main(args=None):
# Start logging
txaio.start_logging(level=os.environ.get("LOGLEVEL", "info"))

parser = make_parser()
args = site_config.parse_args(agent_class='ScpiPsuAgent',
parser=parser,
args=args)

init_params = False
if args.mode == 'acq':
init_params = {'auto_acquire': True}
agent, runner = ocs_agent.init_site_agent(args)

p = ScpiPsuAgent(agent, args.ip_address, int(args.gpib_slot))
p = ScpiPsuAgent(agent, args.ip_address, args.gpib_slot, port=args.port)

agent.register_task('init', p.init)
agent.register_task('init', p.init, startup=init_params)
agent.register_task('set_voltage', p.set_voltage)
agent.register_task('set_current', p.set_current)
agent.register_task('set_output', p.set_output)

agent.register_task('get_voltage', p.get_voltage)
agent.register_task('get_current', p.get_current)
agent.register_task('get_output', p.get_output)

agent.register_process('monitor_output', p.monitor_output, p.stop_monitoring)

runner.run(agent, auto_reconnect=True)
Expand Down
Loading