-
Notifications
You must be signed in to change notification settings - Fork 13
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By convention (and following PEP08) we use CapWords for classnames, so |
||
""" | ||
This device driver is written for the SRS CG635m clock used for the timing system in the SO Office. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Then you can define There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.