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

Added Agent for srs cg635m timing clock. #743

Open
wants to merge 1 commit 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
1 change: 1 addition & 0 deletions socs/agents/ocs_plugin_so.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
('SmurfFileEmulator', 'smurf_file_emulator/agent.py'),
('SmurfStreamSimulator', 'smurf_stream_simulator/agent.py'),
('SmurfTimingCardAgent', 'smurf_timing_card/agent.py'),
('SRSCG635mAgent', 'srs_cg635m/agent.py'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the 'm' in 'SRSCG635m'? Looking at my photos from the site and the SRS website I don't see it as a part of a listed model number, just sometimes in filenames.

('SupRsync', 'suprsync/agent.py'),
('SynaccessAgent', 'synacc/agent.py'),
('SynthAgent', 'holo_synth/agent.py'),
Expand Down
Empty file.
198 changes: 198 additions & 0 deletions socs/agents/srs_cg635m/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import argparse
import socket
import time
from os import environ

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

from socs.agents.srs_cg635m.drivers import SRS_CG635m_Interface


class SRSCG635mAgent:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a docstring.

Also, more generally, we need a docs page for this agent in socs/docs/agents/. Plenty of examples there, but also see https://ocs.readthedocs.io/en/main/developer/agent_references/documentation.html.

This is especially important for deciphering those returned integers.

def __init__(self, agent, ip_address, gpib_slot):
self.agent = agent
self.log = agent.log
self.lock = TimeoutLock()

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

self.clock = None

# Registers Temperature and Voltage feeds
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied comment not relevant here.

agg_params = {
'frame_length': 10 * 60,
}
self.agent.register_feed('srs_clock',
record=True,
agg_params=agg_params,
buffer_time=0)

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

**Task** - Initialize the connection to the srs clock.

Parameters
----------
auto_acquire: bool, optional
Default is False. Starts data acquisition after initialization
if True.
"""
with self.lock.acquire_timeout(0) as acquired:
if not acquired:
return False, "Could not acquire lock"

try:
self.clock = SRS_CG635m_Interface(self.ip_address, self.gpib_slot)
self.idn = self.clock.identify()

except socket.timeout as e:
self.log.error(f"Clock timed out during connect: {e}")
return False, "Timeout"
self.log.info("Connected to Clock: {}".format(self.idn))

# Start data acquisition if requested in site-config
auto_acquire = params.get('auto_acquire', False)
if auto_acquire:
self.agent.start('acq')

return True, 'Initialized Clock.'

@ocs_agent.param('test_mode', default=False, type=bool)
@ocs_agent.param('wait', default=1, type=float)
def acq(self, session, params):
"""acq(wait=1, test_mode=False)

**Process** - Continuously monitor srs clock lock registers
and send info to aggregator.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace "send info to aggregator" with "publish to a feed." The current sentence doesn't really capture that it's also going to other subscribers, like the InfluxDB publisher agent, i.e. to Grafana.


The ``session.data`` object stores the most recent published values
in a dictionary. For example::

session.data = {
'timestamp': 1598626144.5365012,
'block_name': 'clock_output',
'data': {
'Frequency': 122880000.0000000
'Standard_CMOS_Output': 3,
'Running_State': 1,
'PLL_RF_UNLOCKED': 1,
'PLL_19MHZ_UNLOCKED': 1,
'PLL_10MHz_UNLOCKED': 0,
'PLL_RB_UNLOCKED': 1,
'PLL_OUT_UNLOCKED': 0,
'PLL_Phase_Shift': 0,
}
}

Parameters
----------
wait: float, optional
time to wait between measurements [seconds]. Default=1s.

"""
self.monitor = True

while self.monitor:
with self.lock.acquire_timeout(1) as acquired:
if acquired:
data = {
'timestamp': time.time(),
'block_name': 'clock_output',
'data': {}
}

try:
data['data']['Frequency'] = self.clock.get_freq()
data['data']['Standard_CMOS_Output'] = self.clock.get_stdc()
data['data']['Running_State'] = self.clock.get_runs()

# get_lock_statuses returns a dict of the register bits
# Loop through the items to add each to the data
lock_statuses = self.clock.get_lock_statuses()
for register, status in lock_statuses.items():
# Not adding PLL causes a naming error
# Two of the registers start with an number
data['data']["PLL_" + register] = status

except ValueError as e:
self.log.error(f"Error in collecting data: {e}")
continue

self.agent.publish_to_feed('srs_clock', data)

# Allow this process to be queried to return current data
session.data = data

else:
self.log.warn("Could not acquire in monitor clock")

time.sleep(params['wait'])

if params['test_mode']:
break

return True, "Finished monitoring clock"

def _stop_acq(self, session, params):
"""Stop monitoring the clock output."""
if self.monitor:
self.monitor = False
return True, 'requested to stop taking data.'
else:
return False, 'acq is not currently running'


def make_parser(parser=None):
"""Build the argument parser for the Agent. Allows sphinx to automatically
build documentation based on this function.

"""
if parser is None:
parser = argparse.ArgumentParser()

# Add options specific to this agent.
pgroup = parser.add_argument_group('Agent Options')
pgroup.add_argument('--ip-address', type=str, help="Internal GPIB IP Address")
pgroup.add_argument('--gpib-slot', type=int, help="Internal SRS GPIB Address")
pgroup.add_argument('--mode', type=str, help="Set to acq to run acq on "
+ "startup")

return parser


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

parser = site_config.add_arguments()

# Get the default ocs agrument parser
parser = make_parser()

args = site_config.parse_args(agent_class='SRSCG635mAgent',
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 = SRSCG635mAgent(agent, args.ip_address, int(args.gpib_slot))

agent.register_task('init', p.init, startup=init_params)
agent.register_process('acq', p.acq, p._stop_acq)

runner.run(agent, auto_reconnect=True)


if __name__ == '__main__':
main()
129 changes: 129 additions & 0 deletions socs/agents/srs_cg635m/drivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# This device uses the Prologix GPIB interface
from socs.common.prologix_interface import PrologixInterface


class SRS_CG635m_Interface(PrologixInterface):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention (and following PEP08) we use CapWords for classnames, so SRSCG635mInterface.

"""
This device driver is written for the SRS CG635m clock used for the timing system in the SO Office.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop the "in the SO office", that's overly specific.

"""

def __init__(self, ip_address, gpibAddr, verbose=False, **kwargs):
self.verbose = verbose
super().__init__(ip_address, gpibAddr, **kwargs)

def get_freq(self):
"""
Queries the clock for its current output frequency in Hz.

Returns the frequency as a float.
"""

self.write("FREQ?")
freq = self.read()

return float(freq)

def get_stdc(self):
"""
Queries the clock for the current Standard CMOS (STDC) output setting.

The query returns an int with the int representing the CMOS output setting.
The outputs are represented in volts between the CMOS low and CMOS high with CMOS low = 0V.

The standard CMOS output settings this query can return are are:
-1 = Not a standard CMOS Output
0 = 1.2V
1 = 1.8V
2 = 2.5V
3 = 3.3V (The default for our current setup)
4 = 5.0V
"""

self.write("STDC?")
stdc = self.read()

return int(stdc)

def get_runs(self):
"""
Queries the clock for the current Running State (RUNS).

Returns an int which represents the following running states:
0 = Not Running (Output is off)
1 = Running (Output is on)
"""

self.write("RUNS?")
runs = self.read()

return int(runs)

def get_lock_statuses(self):
"""
Queries the clock for the current Lock Registers (LCKR).

The lock registers represent the current Lock status for following registers:
RF_UNLOCK
19MHZ_UNLOCK
10MHZ_UNLOCK
RB_UNLOCK
OUT_DISABLED
PHASE_SHIFT

Returns a dict of the registers and registers statuses with the keys being the registers
and the values being an int representing the register statuses.
1 = True, 0 = False
"""
self.write("LCKR?")
lckr = self.read()

# The LCKR is a 8 bit register with each register status represented by a single bit.
# The LCKR? query returns a single int representation of the register bits
# The decode_lckr function finds the register bit for all registers
lckr_status = self._decode_lckr(lckr)

return lckr_status

def _decode_lckr(self, lckr):
"""
Takes the int representation of the lock register (lckr) and translates it into dict form.
The dict keys are the register names and the values are the register status:
1 = True, 0 = False

The incoming lckr int should always be <256 because its a int representation of an 8 bit reigster.

The lock register bits are as follows:
0 = RF_UNLOCK
1 = 19MHZ_UNLOCK
2 = 10MHZ_UNLOCK
3 = RB_UNLOCK
4 = OUT_DISABLED
5 = PHASE_SHIFT
6 = Reserved
7 = Reserved
"""

registers = {"RF_UNLOCK": None,
"19MHZ_UNLOCK": None,
"10MHZ_UNLOCK": None,
"RB_UNLOCK": None,
"OUT_DISABLED": None,
"PHASE_SHIFT": None}

try:
lckr = int(lckr)

if not 0 <= lckr <= 255:
# If the lckr register is outside of an 8 bit range
raise ValueError

# Decode the lckr int by performing successive int division and subtractionof 2**(5-i)
for i, register in enumerate(list(registers)[::-1]):
register_bit = int(lckr / (2**(5 - i)))
registers[register] = int(register_bit)
lckr -= register_bit * (2**(5 - i))
Comment on lines +121 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do this as is but also could maybe do it a bit more compactly: register_list = [bool(msg & 1 << (8-i)) for i in range(1,9)] I tested this as follows:

In [64]: msg=int('01110000',2);print(msg)
112

In [65]: [bool(msg & 1 << (8-i)) for i in range(1,9)]
Out[65]: [False, True, True, True, False, False, False, False]

In [66]: msg=int('10000000',2);print(msg)
128

In [67]: [bool(msg & 1 << (8-i)) for i in range(1,9)]
Out[67]: [True, False, False, False, False, False, False, False]

In [68]: msg=int('00000001',2);print(msg)
1

In [69]: [bool(msg & 1 << (8-i)) for i in range(1,9)]
Out[69]: [False, False, False, False, False, False, False, True]

Then you can define registers = {rn: rl for r, rl in zip(reg_names, register_list)} where reg_names is a list of the register key names that you've defined in the dict in line 106.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew there was probably a much cleaner way to do this that one of the friendly daq reviewers would surely suggest to me. :)

I can switch this decoding to this way for clarity sake. Though I'd like to keep them as ints so they're easily displayable on grafana, much like the leak detectors.


except ValueError:
print("Invalid LCKR returned, cannot decode")

return registers
1 change: 1 addition & 0 deletions socs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
'SmurfFileEmulator': {'module': 'socs.agents.smurf_file_emulator.agent', 'entry_point': 'main'},
'SmurfStreamSimulator': {'module': 'socs.agents.smurf_stream_simulator.agent', 'entry_point': 'main'},
'SmurfTimingCardAgent': {'module': 'socs.agents.smurf_timing_card.agent', 'entry_point': 'main'},
'SRSCG635mAgent': {'module': 'socs.agents.srs_cg635m.agent', 'entry_point': 'main'},
'SupRsync': {'module': 'socs.agents.suprsync.agent', 'entry_point': 'main'},
'SynaccessAgent': {'module': 'socs.agents.synacc.agent', 'entry_point': 'main'},
'SynthAgent': {'module': 'socs.agents.holo_synth.agent', 'entry_point': 'main'},
Expand Down