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 13 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
14 changes: 14 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,17 @@ pyModbusTCP

# testing
-r requirements/testing.txt

# Contributing
pre-commit

# Ensure compatability with Setuptools 71
packaging>=24
ordered-set>=3.1.1
more_itertools>=8.8
jaraco.text>=3.7
importlib_resources>=5.10.2
importlib_metadata>=6
tomli>=2.0.1
wheel>=0.43.0
platformdirs>=2.6.2
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
138 changes: 125 additions & 13 deletions socs/agents/scpi_psu/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
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


class ScpiPsuAgent:
def __init__(self, agent, ip_address, gpib_slot):
def __init__(self, agent, ip_address, gpib_slot, **kwargs):
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
self.agent = agent
self.log = agent.log
self.lock = TimeoutLock()

self.job = None
self.ip_address = ip_address
self.gpib_slot = gpib_slot
self.port = None
if ('port' in kwargs.keys()):
self.port = kwargs['port']
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
self.monitor = False

self.psu = None
Expand All @@ -30,7 +33,7 @@ def __init__(self, agent, ip_address, gpib_slot):
agg_params=agg_params,
buffer_time=0)

@ocs_agent.param('_')
# @ocs_agent.param('_')
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved
def init(self, session, params=None):
"""init()

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

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"
if 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, self.gpib_slot, 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.error(f"PSU initialization error: {e}. Suggest appending {e.args[-1]} to the list of known model numbers in scpi_psu/drivers.py")
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved
else:
self.log.error(f"PSU initialization resulted in unknown ValueError: {e}")
return False, "ValueError"

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

session.add_message('Initialized PSU.')
auto_acquire = params.get('auto_acquire', False)

if auto_acquire:
acq_params = None
if self.psu.numChannels == 1:
acq_params = {'channels': [1]}
self.agent.start('monitor_output', acq_params)
return True, 'Initialized PSU.'

@ocs_agent.param('wait', type=float, default=1)
Expand All @@ -68,6 +94,7 @@ def monitor_output(self, session, params=None):
Defaults to False.

"""
session.set_status('running')
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved
self.monitor = True

while self.monitor:
Expand All @@ -80,8 +107,13 @@ def monitor_output(self, session, params=None):
}

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

# self.log.info(str(data))
# print(data)
Expand All @@ -104,6 +136,31 @@ 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).

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

data['data']['Voltage_{}'.format(chan)] = self.psu.get_volt(chan)
session.data = data
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -125,6 +182,30 @@ 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).

"""
chan = params['channel']
with self.lock.acquire_timeout(1) as acquired:
if acquired:
data = {
'timestamp': time.time(),
'block_name': 'output',
'data': {}
}
data['data']['Current_{}'.format(chan)] = self.psu.get_curr(chan)
session.data = data
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -145,6 +226,26 @@ 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):
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
"""
enabled = False
with self.lock.acquire_timeout(1) as acquired:
if acquired:
enabled = self.psu.get_output(params['channel'])
else:
return False, "Could not acquire lock."
if enabled:
return True, 'Channel {} output is currently enabled.'.format(params['channel'])
else:
return True, 'Channel {} output is currently disabled.'.format(params['channel'])
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved

@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 @@ -178,6 +279,9 @@ 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

Expand All @@ -188,15 +292,23 @@ def main(args=None):
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, int(args.gpib_slot), port=int(args.port))
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved

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)

# Need tasks for get_voltage, get_current, and get_output
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved
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
144 changes: 144 additions & 0 deletions socs/agents/scpi_psu/drivers.py
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,150 @@
# Tucker Elleflot

import socket
import time

from socs.common.prologix_interface import PrologixInterface

# append new model strings as needed
ONE_CHANNEL_MODELS = ['2280S-60-3', '2280S-32-6']
THREE_CHANNEL_MODELS = ['2230G-30-1']


class ScpiPsuInterface:
def __init__(self, ip_address, gpibAddr, port, **kwargs):
self.ip_address = ip_address
self.gpibAddr = gpibAddr
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved
self.port = port
self.sock = None
self.model = None
self.numChannels = 0
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved
self.conn_socket()
try:
self.configure()
except ValueError as err:
raise ValueError(err)
super().__init__(**kwargs)
CAWNoodle marked this conversation as resolved.
Show resolved Hide resolved

def conn_socket(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.ip_address, self.port))
self.sock.settimeout(5)

def read(self):
return self.sock.recv(128).decode().strip()

def write(self, msg):
message = msg + '\n'
self.sock.sendall(message.encode())
time.sleep(0.1) # to prevent flooding the connection

def identify(self):
self.write('*idn?')
return self.read()

def read_model(self):
idn_response = self.identify().split(',')[1]
if (idn_response.startswith('MODEL')):
return idn_response[6:]
else:
return idn_response

def configure(self):
self.model = self.read_model()
if (self.model in ONE_CHANNEL_MODELS):
self.numChannels = 1
if (self.model in THREE_CHANNEL_MODELS):
self.numChannels = 3
if (self.numChannels == 0):
raise ValueError('Model number not found in known device models', self.model)
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved

def enable(self, ch):
'''
Enables output for channel (1,2,3) but does not turn it on.
Depending on state of power supply, it might need to be called
before the output is set.
'''
if (self.numChannels != 1):
self.set_chan(ch)
self.write('OUTP:ENAB ON')

def disable(self, ch):
'''
disabled output from a channel (1,2,3). once called, enable must be
called to turn on the channel again
'''
self.write('OUTP:ENAB OFF')
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved

def set_chan(self, ch):
if (self.numChannels != 1):
self.write('inst:nsel ' + str(ch))

def set_output(self, ch, out):
'''
set status of power supply channel
ch - channel (1,2,3) to set status
out - ON: True|1|'ON' OFF: False|0|'OFF'

Calls enable to ensure a channel can be turned on. We might want to
make them separate (and let us use disable as a safety feature) but
for now I am thinking we just want to thing to turn on when we tell
it to turn on.
'''
self.set_chan(ch)
self.enable(ch)
if (self.numChannels != 1):
if isinstance(out, str):
self.write('CHAN:OUTP ' + out)
elif out:
self.write('CHAN:OUTP ON')
else:
self.write('CHAN:OUTP OFF')
else:
if isinstance(out, str):
self.write('OUTP ' + out)
elif out:
self.write('OUTP ON')
else:
self.write('OUTP OFF')
BrianJKoopman marked this conversation as resolved.
Show resolved Hide resolved

def get_output(self, ch):
'''
check if the output of a channel (1,2,3) is on (True) or off (False)
'''
self.set_chan(ch)
if (self.numChannels != 1):
self.write('CHAN:OUTP:STAT?')
else:
self.write('OUTP:STAT?')
out = bool(float(self.read()))
return out

def set_volt(self, ch, volt):
self.set_chan(ch)
self.write('volt ' + str(volt))

def set_curr(self, ch, curr):
self.set_chan(ch)
self.write('curr ' + str(curr))

def get_volt(self, ch):
self.set_chan(ch)
if (self.numChannels != 1):
self.write('MEAS:VOLT? CH' + str(ch))
else:
self.write('MEAS:VOLT?')
voltage = float(self.read().split(',')[1].strip('V'))
return voltage

def get_curr(self, ch):
self.set_chan(ch)
if (self.numChannels != 1):
self.write('MEAS:CURR? CH' + str(ch))
else:
self.write('MEAS:CURR?')
current = float(self.read().split(',')[0].strip('A'))
return current


class PsuInterface(PrologixInterface):
def __init__(self, ip_address, gpibAddr, verbose=False, **kwargs):
Expand Down
Loading