• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

simonsobs / socs / 15856342507

24 Jun 2025 04:39PM UTC coverage: 38.282% (-0.03%) from 38.313%
15856342507

Pull #896

github

web-flow
Merge 6b8215ad5 into 0e21b9665
Pull Request #896: feat(agents.pysmurf_controller): Return status after RssiRestart.

0 of 12 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

7225 of 18873 relevant lines covered (38.28%)

0.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.4
/socs/agents/pysmurf_controller/agent.py
1
from dataclasses import asdict
1✔
2

3
import matplotlib
1✔
4
from autobahn.twisted.util import sleep as dsleep
1✔
5
from twisted.internet import protocol, reactor, threads
1✔
6
from twisted.internet.defer import Deferred, inlineCallbacks
1✔
7
from twisted.logger import FileLogObserver, Logger
1✔
8
from twisted.python.failure import Failure
1✔
9

10
matplotlib.use('Agg')
1✔
11
import argparse
1✔
12
import os
1✔
13
import sys
1✔
14
import time
1✔
15
from typing import Optional
1✔
16

17
import epics
1✔
18
import numpy as np
1✔
19
import sodetlib as sdl
1✔
20
from ocs import ocs_agent, site_config
1✔
21
from ocs.ocs_agent import log_formatter
1✔
22
from ocs.ocs_twisted import TimeoutLock
1✔
23
from sodetlib.det_config import DetConfig
1✔
24
from sodetlib.operations import bias_dets
1✔
25

26
from socs.agents.pysmurf_controller.smurf_subprocess_util import (
1✔
27
    QuantileData, RunCfg, RunResult, run_smurf_func)
28

29

30
class PysmurfScriptProtocol(protocol.ProcessProtocol):
1✔
31
    """
32
    The process protocol used to dispatch external Pysmurf scripts, and manage
33
    the stdin, stdout, and stderr pipelines.
34

35
    Arguments
36
    ---------
37
    path : str
38
        Path of script to run.
39
    log : txaio.tx.Logger
40
        txaio logger object, used to log stdout and stderr messages.
41

42
    Attributes
43
    -----------
44
    path : str
45
        Path of script to run.
46
    log : txaio.tx.Logger
47
        txaio logger object, used to log stdout and stderr messages.
48
    end_status : twisted.python.failure.Failure
49
        Reason that the process ended.
50
    """
51

52
    def __init__(self, path, log=None):
1✔
53
        self.path = path
×
54
        self.log = log
×
55
        self.end_status: Optional[Failure] = None
×
56

57
    def connectionMade(self):
1✔
58
        """Called when process is started"""
59
        self.transport.closeStdin()
×
60

61
    def outReceived(self, data):
1✔
62
        """Called whenever data is received through stdout"""
63
        if self.log:
×
64
            self.log.info("{path}: {data}",
×
65
                          path=self.path.split('/')[-1],
66
                          data=data.strip().decode('utf-8'))
67

68
    def errReceived(self, data):
1✔
69
        """Called whenever data is received through stderr"""
70
        self.log.error(data)
×
71

72
    def processExited(self, status: Failure):
1✔
73
        """Called when process has exited."""
74

75
        rc = status.value.exitCode
×
76
        if self.log is not None:
×
77
            self.log.info("Process ended with exit code {rc}", rc=rc)
×
78

79
        self.deferred.callback(rc)
×
80

81

82
def set_session_data(session, result: RunResult):
1✔
83
    """Sets session data based on a RunResult object"""
84
    if result.return_val is not None:
1✔
85
        if isinstance(result.return_val, dict):
1✔
86
            session.data = result.return_val
1✔
87
    session.data['result'] = asdict(result)
1✔
88

89

90
class PysmurfController:
1✔
91
    """
92
    Controller object for running pysmurf scripts and functions.
93

94
    Args:
95
        agent (ocs.ocs_agent.OCSAgent):
96
            OCSAgent object which is running
97
        args (Namespace):
98
            argparse namespace with site_config and agent specific arguments
99

100
    Attributes:
101
        agent (ocs.ocs_agent.OCSAgent):
102
            OCSAgent object which is running
103
        log (txaio.tx.Logger):
104
            txaio logger object created by agent
105
        prot (PysmurfScriptProtocol):
106
            protocol used to call and monitor external pysmurf scripts
107
        lock (ocs.ocs_twisted.TimeoutLock):
108
            lock to protect multiple pysmurf scripts from running simultaneously.
109
        slot (int):
110
            ATCA Slot of the smurf-card this agent is commanding.
111
    """
112

113
    def __init__(self, agent, args):
1✔
114
        self.agent: ocs_agent.OCSAgent = agent
1✔
115
        self.log = agent.log
1✔
116

117
        self.prot = None
1✔
118
        self.lock = TimeoutLock()
1✔
119

120
        self.current_session = None
1✔
121

122
        self.slot = args.slot
1✔
123
        if self.slot is None:
1✔
124
            self.slot = os.environ['SLOT']
×
125

126
        if args.monitor_id is not None:
1✔
127
            self.agent.subscribe_on_start(
1✔
128
                self._on_session_data,
129
                'observatory.{}.feeds.pysmurf_session_data'.format(args.monitor_id),
130
            )
131

132
        self.agent.register_feed('tracking_results', record=True)
1✔
133
        self.agent.register_feed('bias_step_results', record=True)
1✔
134
        self.agent.register_feed('noise_results', record=True)
1✔
135
        self.agent.register_feed('iv_results', record=True)
1✔
136
        self.agent.register_feed('bias_wave_results', record=True)
1✔
137
        self.agent.register_feed('state_results', record=True)
1✔
138

139
    def _on_session_data(self, _data):
1✔
140
        data, feed = _data
×
141

142
        if self.current_session is not None:
×
143
            if data['id'] == os.environ.get("SMURFPUB_ID"):
×
144
                if data['type'] == 'session_data':
×
145
                    if isinstance(data['payload'], dict):
×
146
                        self.current_session.data.update(data['payload'])
×
147
                    else:
148
                        self.log.warn("Session data not passed as a dict!! Skipping...")
×
149

150
                elif data['type'] == 'session_log':
×
151
                    if isinstance(data['payload'], str):
×
152
                        self.current_session.add_message(data['payload'])
×
153

154
    def _get_smurf_control(self, session=None, load_tune=True, **kwargs):
1✔
155
        """
156
        Gets pysmurf and det-config instances for sodetlib functions.
157
        """
158
        cfg = DetConfig()
×
159
        cfg.load_config_files(slot=self.slot)
×
160
        S = cfg.get_smurf_control(**kwargs)
×
161
        if load_tune:
×
162
            S.load_tune(cfg.dev.exp['tunefile'])
×
163
        S._ocs_session = session
×
164
        return S, cfg
×
165

166
    @inlineCallbacks
1✔
167
    def _run_script(self, script, args, log, session):
1✔
168
        """
169
        Runs a pysmurf control script. Can only run from the reactor.
170

171
        Args:
172
            script (string):
173
                path to the script you wish to run
174
            args (list, optional):
175
                List of command line arguments to pass to the script.
176
                Defaults to [].
177
            log (string or bool, optional):
178
                Determines if and how the process's stdout should be logged.
179
                You can pass the path to a logfile, True to use the agent's log,
180
                or False to not log at all.
181
        """
182

183
        with self.lock.acquire_timeout(0, job=script) as acquired:
×
184
            if not acquired:
×
185
                return False, "The requested script cannot be run because " \
×
186
                              "script {} is already running".format(self.lock.job)
187

188
            self.current_session = session
×
189
            try:
×
190
                # IO  is not really safe from the reactor thread, so we possibly
191
                # need to find another way to do this if people use it and it
192
                # causes problems...
193
                logger = None
×
194
                if isinstance(log, str):
×
195
                    self.log.info("Logging output to file {}".format(log))
×
196
                    log_file = yield threads.deferToThread(open, log, 'a')
×
197
                    logger = Logger(observer=FileLogObserver(log_file, log_formatter))
×
198
                elif log:
×
199
                    # If log==True, use agent's logger
200
                    logger = self.log
×
201

202
                self.prot = PysmurfScriptProtocol(script, log=logger)
×
203
                self.prot.deferred = Deferred()
×
204
                python_exec = sys.executable
×
205

206
                cmd = [python_exec, '-u', script] + list(map(str, args))
×
207

208
                self.log.info("{exec}, {cmd}", exec=python_exec, cmd=cmd)
×
209

210
                reactor.spawnProcess(self.prot, python_exec, cmd, env=os.environ)
×
211

212
                rc = yield self.prot.deferred
×
213

214
                return (rc == 0), "Script has finished with exit code {}".format(rc)
×
215

216
            finally:
217
                # Sleep to allow any remaining messages to be put into the
218
                # session var
219
                yield dsleep(1.0)
×
220
                self.current_session = None
×
221

222
    @inlineCallbacks
1✔
223
    def run(self, session, params=None):
1✔
224
        """run(script, args=[], log=True)
225

226
        **Task** - Runs a pysmurf control script.
227

228
        Parameters:
229
            script (string):
230
                Path of the pysmurf script to run.
231
            args (list, optional):
232
                List of command line arguments to pass to the script.  Defaults
233
                to [].
234
            log (string/bool, optional):
235
                Determines if and how the process's stdout should be logged.
236
                You can pass the path to a logfile, True to use the agent's
237
                log, or False to not log at all.
238

239
        Notes:
240
            Data and logs may be passed from the pysmurf control script to the
241
            session object by publishing it via the Pysmurf Publisher using the
242
            message types ``session_data`` and ``session_logs`` respectively.
243

244
            For example, below is a simple script which starts the data stream
245
            and returns the datfile path and the list of active channels to the
246
            session::
247

248
                active_channels = S.which_on(0)
249
                datafile = S.stream_data_on()
250
                S.pub.publish({
251
                    'datafile': datafile, 'active_channels': active_channels
252
                }, msgtype='session_data')
253

254
            This would result in the following session.data object::
255

256
                >>> response.session['data']
257
                {
258
                    'datafile': '/data/smurf_data/20200316/1584401673/outputs/1584402020.dat',
259
                    'active_channels': [0,1,2,3,4]
260
                }
261

262
        """
263
        ok, msg = yield self._run_script(
×
264
            params['script'],
265
            params.get('args', []),
266
            params.get('log', True),
267
            session
268
        )
269

270
        return ok, msg
×
271

272
    def abort(self, session, params=None):
1✔
273
        """abort()
274

275
        **Task** - Aborts the actively running script.
276

277
        """
278
        self.prot.transport.signalProcess('KILL')
×
279
        return True, "Aborting process"
×
280

281
    @ocs_agent.param('poll_interval', type=float, default=10)
1✔
282
    @ocs_agent.param('test_mode', default=False, type=bool)
1✔
283
    def check_state(self, session, params=None):
1✔
284
        """check_state(poll_interval=10, test_mode=False)
285

286
        **Process** - Continuously checks the current state of the smurf. This
287
        will not modify the smurf state, so this task can be run in conjunction
288
        with other smurf operations. This will continuously poll smurf metadata
289
        and update the ``session.data`` object.
290

291
        Args
292
        -----
293
        poll_interval : float
294
            Time (sec) between updates.
295
        test_mode : bool, optional
296
            Run the Process loop only once. This is meant only for testing.
297
            Default is False.
298

299
        Notes
300
        -------
301
        The following data will be written to the session.data object::
302

303
            >> response.session['data']
304
            {
305
                'channel_mask': Array of channels that are streaming data,
306
                'downsample_factor': downsample_factor,
307
                'agg_time': Buffer time per G3Frame (sec),
308
                'open_g3stream': True if data is currently streaming to G3,
309
                'pysmurf_action': Current pysmurf action,
310
                'pysmurf_action_timestamp': Current pysmurf-action timestamp,
311
                'stream_tag': stream-tag for the current g3 stream,
312
                'last_update':  Time that session-data was last updated,
313
                'stream_id': Stream-id of the controlled smurf instance,
314
                'num_active_channels': Number of channels outputting tones
315
            }
316
        """
317
        S, cfg = self._get_smurf_control(load_tune=False, no_dir=True)
1✔
318
        reg = sdl.Registers(S)
1✔
319

320
        kw = {'retry_on_fail': False}
1✔
321
        while session.status in ['starting', 'running']:
1✔
322
            try:
1✔
323

324
                num_active_channels = 0
1✔
325
                for band in range(8):
1✔
326
                    num_active_channels += len(S.which_on(band))
1✔
327

328
                d = dict(
1✔
329
                    channel_mask=S.get_channel_mask(**kw).tolist(),
330
                    downsample_factor=S.get_downsample_factor(**kw),
331
                    agg_time=reg.agg_time.get(**kw),
332
                    open_g3stream=reg.open_g3stream.get(**kw),
333
                    pysmurf_action=reg.pysmurf_action.get(**kw, as_string=True),
334
                    pysmurf_action_timestamp=reg.pysmurf_action_timestamp.get(**kw),
335
                    stream_tag=reg.stream_tag.get(**kw, as_string=True),
336
                    last_update=time.time(),
337
                    stream_id=cfg.stream_id,
338
                    num_active_channels=num_active_channels,
339
                )
340
                session.data.update(d)
1✔
341

342
                data = {
1✔
343
                    'timestamp': time.time(),
344
                    'block_name': 'state_results',
345
                    'data': {'open_g3stream': d['open_g3stream'],
346
                             'num_active_channels': d['num_active_channels']}
347
                }
348
                self.agent.publish_to_feed('state_results', data)
1✔
349
            except (RuntimeError, epics.ca.ChannelAccessGetFailure):
×
350
                self.log.warn("Could not connect to epics server! Waiting and "
×
351
                              "then trying again")
352

353
            time.sleep(params['poll_interval'])
1✔
354

355
            if params['test_mode']:
1✔
356
                break
1✔
357

358
        return True, "Finished checking state"
1✔
359

360
    def _stop_check_state(self, session, params):
1✔
361
        """Stopper for check state process"""
362
        session.set_status('stopping')
×
363

364
    def run_test_func(self, session, params):
1✔
365
        """run_test_func()
366

367
        **Task** - Task to test the subprocessing functionality without any
368
        smurf hardware.
369
        """
370
        cfg = RunCfg(
×
371
            func_name='test',
372
        )
373
        result = run_smurf_func(cfg)
×
374
        if not result.success:
×
375
            self.log.error("Subprocess errored out:\n{tb}", tb=result.traceback)
×
376

377
        if isinstance(result.return_val, dict):
×
378
            session.data = result.return_val
×
379
        else:
380
            session.data['data'] = result.return_val
×
381

382
        return result.success, 'finished'
×
383

384
    @ocs_agent.param("duration", default=None, type=float)
1✔
385
    @ocs_agent.param('kwargs', default=None)
1✔
386
    @ocs_agent.param('load_tune', default=True, type=bool)
1✔
387
    @ocs_agent.param('stream_type', default='obs', choices=['obs', 'oper'])
1✔
388
    @ocs_agent.param('subtype', default=None)
1✔
389
    @ocs_agent.param('tag', default=None)
1✔
390
    @ocs_agent.param('test_mode', default=False, type=bool)
1✔
391
    def stream(self, session, params):
1✔
392
        """stream(duration=None, kwargs=None, load_tune=True, \
393
                  stream_type='obs', subtype=None, tag=None, test_mode=False)
394

395
        **Process** - Stream smurf data. If a duration is specified, stream
396
        will end after that amount of time. If unspecified, the stream will run
397
        until the stop function is called.
398

399
        Args
400
        -----
401
        duration : float, optional
402
            If set, determines how many seconds to stream data. By default,
403
            will leave stream open until stop function is called.
404
        kwargs : dict
405
            A dictionary containing additional keyword arguments to pass
406
            to sodetlib's ``stream_g3_on`` function
407
        load_tune : bool
408
            If true, will load a tune-file to the pysmurf object on
409
            instantiation.
410
        stream_type : string, optional
411
            Stream type. This can be either 'obs' or 'oper', and will be 'obs'
412
            by default. The tags attached to the stream will be
413
            ``<stream_type>,<subtype>,<tag>``.
414
        subtype : string, optional
415
            Operation subtype used tot tag the stream.
416
        tag : string, optional
417
            Tag (or comma-separated list of tags) to attach to the G3 stream.
418
            This has precedence over the `tag` key in the kwargs dict.
419
        test_mode : bool, optional
420
            Run the Process loop only once. This is meant only for testing.
421
            Default is False.
422

423
        Notes
424
        ------
425
        The following data will be written to the session.data object::
426

427
            >> response.session['data']
428
            {
429
                'stream_id': Stream-id for the slot,
430
                'sid': Session-id for the streaming session,
431
            }
432
        """
433
        if params['kwargs'] is None:
1✔
434
            params['kwargs'] = {}
1✔
435

436
        if params['stream_type']:
1✔
437
            params['kwargs']['tag'] = params['stream_type']
1✔
438
        if params['subtype'] is not None:
1✔
439
            params['kwargs']['subtype'] = params['subtype']
×
440
        if params['tag'] is not None:
1✔
441
            params['kwargs']['tag'] = params['tag']
×
442

443
        with self.lock.acquire_timeout(0, job='stream') as acquired:
1✔
444
            if not acquired:
1✔
445
                return False, f"Operation failed: {self.lock.job} is running."
×
446

447
            S, cfg = self._get_smurf_control(session=session,
1✔
448
                                             load_tune=params['load_tune'])
449

450
            stop_time = None
1✔
451
            if params['duration'] is not None:
1✔
452
                stop_time = time.time() + params['duration']
×
453

454
            session.data['stream_id'] = cfg.stream_id
1✔
455
            session.data['sid'] = sdl.stream_g3_on(S, **params['kwargs'])
1✔
456
            while session.status in ['starting', 'running']:
1✔
457
                if stop_time is not None:
1✔
458
                    if time.time() > stop_time:
×
459
                        break
×
460
                time.sleep(1)
1✔
461

462
                if params['test_mode']:
1✔
463
                    break
1✔
464
            sdl.stream_g3_off(S)
1✔
465

466
        return True, 'Finished streaming data'
1✔
467

468
    def _stream_stop(self, session, params=None):
1✔
469
        session.set_status('stopping')
×
470
        return True, "Requesting to end stream"
×
471

472
    @ocs_agent.param('bands', default=None)
1✔
473
    @ocs_agent.param('kwargs', default=None)
1✔
474
    @ocs_agent.param('run_in_main_procsess', default=False)
1✔
475
    def uxm_setup(self, session, params):
1✔
476
        """uxm_setup(bands=None, kwargs=None, run_in_main_process=False)
477

478
        **Task** - Runs first-time setup procedure for a UXM. This will run the
479
        following operations:
480

481
            1. Setup Amps (~1 min)
482
            2. Estimate attens if attens are not already set in the device cfg
483
               (~1 min / band)
484
            3. Estimate phase delay (~1 min / band)
485
            4. Setup tune (~7 min / band)
486
            5. Setup tracking param (~30s / band)
487
            6. Measure noise (~45s)
488

489
        See the `sodetlib setup docs
490
        <https://simons1.princeton.edu/docs/sodetlib/operations/setup.html#first-time-setup>`_
491
        for more information on the sodetlib setup procedure and allowed
492
        keyword arguments.
493

494
        Args
495
        -----
496
        bands : list, int
497
            Bands to set up. Defaults to all.
498
        kwargs : dict
499
            Dict containing additional keyword args to pass to the uxm_setup
500
            function.
501
        run_in_main_process : bool
502
            If true, run smurf-function in main process. Mainly for the purpose
503
            of testing without the reactor running.
504

505
        Notes
506
        -------
507
        SODETLIB functions such as ``uxm_setup`` and any other functions called
508
        by ``uxm_setup`` will add relevant data to the ``session.data`` object
509
        to  a unique key. For example, if all is successful ``session.data``
510
        may look like::
511

512
            >> response.session['data']
513
            {
514
                'timestamps': [('setup_amps', 1651162263.0204525), ...],
515
                'setup_amps_summary': {
516
                   'success': True,
517
                   'amp_50k_Id': 15.0,
518
                   'amp_hemt_Id': 8.0,
519
                   'amp_50k_Vg': -0.52,
520
                   'amp_hemt_Vg': -0.829,
521
                },
522
                'setup_phase_delay': {
523
                    'bands': [0, 1, ...]
524
                    'band_delay_us': List of band delays
525
                },
526
                'noise': {
527
                   'band_medians': List of median white noise for each band
528
                }
529
            }
530
        """
531
        if params['kwargs'] is None:
1✔
532
            params['kwargs'] = {}
1✔
533

534
        with self.lock.acquire_timeout(0, job='uxm_setup') as acquired:
1✔
535
            if not acquired:
1✔
536
                return False, f"Operation failed: {self.lock.job} is running."
×
537

538
            cfg = RunCfg(
1✔
539
                func_name='run_uxm_setup',
540
                kwargs={'bands': params['bands'], 'kwargs': params['kwargs']},
541
                run_in_main_process=params['run_in_main_process'],
542
            )
543
            result = run_smurf_func(cfg)
1✔
544
            set_session_data(session, result)
1✔
545
            if result.traceback is not None:
1✔
546
                self.log.error("Error occurred:\n{tb}", tb=result.traceback)
×
547
            return result.success, "Finished UXM Setup"
1✔
548

549
    @ocs_agent.param('bands', default=None)
1✔
550
    @ocs_agent.param('kwargs', default=None)
1✔
551
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
552
    def uxm_relock(self, session, params):
1✔
553
        """uxm_relock(bands=None, kwargs=None, run_in_main_process=False)
554

555
        **Task** - Relocks detectors to existing tune if setup has already been
556
        run. Runs the following operations:
557

558
            1. Setup Amps (~1 min)
559
            2. Relocks detectors, setup notches (if requested), and serial
560
               gradient descent / eta scan (~5 min / band)
561
            3. Tracking setup (~20s / band)
562
            4. Noise check (~45s)
563

564
        See the `sodetlib relock docs
565
        <https://simons1.princeton.edu/docs/sodetlib/operations/setup.html#relocking>`_
566
        for more information on the sodetlib relock procedure and allowed
567
        keyword arguments.
568

569
        Args
570
        -----
571
        bands : list, int
572
            Bands to set up. Defaults to all.
573
        kwargs : dict
574
            Dict containing additional keyword args to pass to the uxm_relock
575
            function.
576
        run_in_main_process : bool
577
            If true, run smurf-function in main process. Mainly for the purpose
578
            of testing without the reactor running.
579

580
        Notes
581
        -------
582
        SODETLIB functions such as ``uxm_relock`` and any other functions called
583
        will add relevant data to the ``session.data`` object to  a unique key.
584
        For example, if all is successful ``session.data`` may look like::
585

586
            >> response.session['data']
587
            {
588
                'filepath': Filepath of saved TrackingResults object
589
                'all_det_num': Total number of detectors for each band
590
                'good_det_num': Total number of good tracking detectors for each band
591
                }
592
            }
593
        """
594
        if params['kwargs'] is None:
1✔
595
            params['kwargs'] = {}
1✔
596

597
        with self.lock.acquire_timeout(0, job='uxm_relock') as acquired:
1✔
598
            if not acquired:
1✔
599
                return False, f"Operation failed: {self.lock.job} is running."
×
600

601
            cfg = RunCfg(
1✔
602
                func_name='run_uxm_relock',
603
                kwargs={'bands': params['bands'], 'kwargs': params['kwargs']},
604
                run_in_main_process=params['run_in_main_process'],
605
            )
606
            result = run_smurf_func(cfg)
1✔
607
            set_session_data(session, result)
1✔
608
            if result.success:
1✔
609
                block_data = {}
1✔
610
                for iband, (iall, igood) in enumerate(zip(result.return_val['all_det_num'], result.return_val['good_det_num'])):
1✔
611
                    block_data[f'alldet_band{iband}'] = iall
1✔
612
                    block_data[f'gooddet_band{iband}'] = igood
1✔
613
                data = {
1✔
614
                    'timestamp': time.time(),
615
                    'block_name': 'tracking_results',
616
                    'data': block_data
617
                }
618
                self.agent.publish_to_feed('tracking_results', data)
1✔
619
            if result.traceback is not None:
1✔
620
                self.log.error("Error occurred:\n{tb}", tb=result.traceback)
×
621

622
            return result.success, "Finished UXM Relock"
1✔
623

624
    @ocs_agent.param('duration', default=30., type=float)
1✔
625
    @ocs_agent.param('kwargs', default=None)
1✔
626
    @ocs_agent.param('tag', default=None)
1✔
627
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
628
    def take_noise(self, session, params):
1✔
629
        """take_noise(duration=30., kwargs=None, tag=None, run_in_main_process=False)
630

631
        **Task** - Takes a short timestream and calculates noise statistics.
632
        Median white noise level for each band will be stored in the session
633
        data. See the `sodetlib noise docs
634
        <https://simons1.princeton.edu/docs/sodetlib/noise.html>`_ for more
635
        information on the noise function and possible keyword arguments.
636

637
        Args
638
        -----
639
        duration : float
640
            Duration of timestream to take for noise calculation.
641
        kwargs : dict
642
            Dict containing additional keyword args to pass to the take_noise
643
            function.
644
        tag : string, optional
645
            Tag (or comma-separated list of tags) to attach to the G3 stream.
646
            This has precedence over the `tag` key in the kwargs dict.
647
        run_in_main_process : bool
648
            If true, run smurf-function in main process. Mainly for the purpose
649
            of testing without the reactor running.
650

651
        Notes
652
        -------
653
        Median white noise levels for each band will be stored in the
654
        session.data object, for example::
655

656
            >> response.session['data']
657
            {
658
                'noise': {
659
                   'band_medians': List of median white noise for each band
660
                }
661
            }
662
        """
663
        if params['kwargs'] is None:
1✔
664
            params['kwargs'] = {}
1✔
665

666
        if params['tag'] is not None:
1✔
667
            params['kwargs']['g3_tag'] = params['tag']
×
668

669
        with self.lock.acquire_timeout(0, job='take_noise') as acquired:
1✔
670
            if not acquired:
1✔
671
                return False, f"Operation failed: {self.lock.job} is running."
×
672

673
            cfg = RunCfg(
1✔
674
                func_name='take_noise',
675
                args=[params['duration']],
676
                kwargs={'kwargs': params['kwargs']},
677
                run_in_main_process=params['run_in_main_process'],
678
            )
679

680
            result = run_smurf_func(cfg)
1✔
681
            set_session_data(session, result)
1✔
682
            if result.success:
1✔
683
                block_data = {}
1✔
684
                for qd in result.return_val['quantiles'].values():
1✔
685
                    if isinstance(qd, dict):
1✔
686
                        qd = QuantileData(**qd)
×
687
                    block_data.update(qd.to_block_data())
1✔
688
                d = {
1✔
689
                    'timestamp': time.time(),
690
                    'block_name': 'noise_results',
691
                    'data': block_data
692
                }
693
                self.agent.publish_to_feed('noise_results', d)
1✔
694
            return result.success, "Finished taking noise"
1✔
695

696
    @ocs_agent.param('kwargs', default=None)
1✔
697
    @ocs_agent.param('tag', default=None)
1✔
698
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
699
    def take_bgmap(self, session, params):
1✔
700
        """take_bgmap(kwargs=None, tag=None, run_in_main_process=False)
701

702
        **Task** - Takes a bias-group map. This will calculate the number of
703
        channels assigned to each bias group and put that into the session data
704
        object along with the filepath to the analyzed bias-step output. See
705
        the `bias steps docs page <https://simons1.princeton.edu/docs/sodetlib/operations/bias_steps.html>`_
706
        for more information on what additional keyword arguments can be
707
        passed.
708

709
        Args
710
        ----
711
        kwargs : dict
712
            Additional kwargs to pass to take_bgmap function.
713
        tag : Optional[str]
714
            String containing a tag or comma-separated list of tags to attach
715
            to the g3 stream.
716
        run_in_main_process : bool
717
            If true, run smurf-function in main process. Mainly for the purpose
718
            of testing without the reactor running.
719

720
        Notes
721
        ------
722
        The filepath of the BiasStepAnalysis object and the number of channels
723
        assigned to each bias group will be written to the session.data
724
        object::
725

726
            >> response.session['data']
727
            {
728
                'nchans_per_bg': [123, 183, 0, 87, ...],
729
                'filepath': /path/to/bias_step_file/on/smurf_server.npy,
730
            }
731

732
        """
733
        if params['kwargs'] is None:
1✔
734
            params['kwargs'] = {}
×
735

736
        if params['tag'] is not None:
1✔
737
            params['kwargs']['g3_tag'] = params['tag']
×
738

739
        with self.lock.acquire_timeout(0, job='take_bgmap') as acquired:
1✔
740
            if not acquired:
1✔
741
                return False, f"Operation failed: {self.lock.job} is running."
×
742

743
            kwargs = {
1✔
744
                'show_plots': False,
745
            }
746
            kwargs.update(params['kwargs'])
1✔
747
            cfg = RunCfg(
1✔
748
                func_name='take_bgmap',
749
                kwargs={'kwargs': kwargs},
750
                run_in_main_process=params['run_in_main_process'],
751
            )
752
            result = run_smurf_func(cfg)
1✔
753
            set_session_data(session, result)
1✔
754
            return result.success, "Finished taking bgmap"
1✔
755

756
    @ocs_agent.param('kwargs', default=None)
1✔
757
    @ocs_agent.param('tag', default=None)
1✔
758
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
759
    def take_iv(self, session, params):
1✔
760
        """take_iv(kwargs=None, tag=None, run_in_main_process=False)
761

762
        **Task** - Takes an IV. This will add the normal resistance array and
763
        channel info to the session data object along with the analyzed IV
764
        filepath. See the `sodetlib IV docs page
765
        <https://simons1.princeton.edu/docs/sodetlib/operations/iv.html>`_
766
        for more information on what additional keyword arguments can be passed
767
        in.
768

769
        Args
770
        ----
771
        kwargs : dict
772
            Additional kwargs to pass to the ``take_iv`` function.
773
        tag : Optional[str]
774
            String containing a tag or comma-separated list of tags to attach
775
            to the g3 stream.
776
        run_in_main_process : bool
777
            If true, run smurf-function in main process. Mainly for the purpose
778
            of testing without the reactor running.
779

780
        Notes
781
        ------
782
        The following data will be written to the session.data object::
783

784
            >> response.session['data']
785
            {
786
                'filepath': Filepath of saved IVAnalysis object
787
                'quantiles': {
788
                    'Rn': Rn quantiles
789
                    'p_sat': electrical power at 90% Rn quantiles
790
                }
791
            }
792
        """
793
        if params['kwargs'] is None:
1✔
794
            params['kwargs'] = {}
×
795

796
        if params['tag'] is not None:
1✔
797
            params['kwargs']['g3_tag'] = params['tag']
×
798

799
        with self.lock.acquire_timeout(0, job='take_iv') as acquired:
1✔
800
            if not acquired:
1✔
801
                return False, f"Operation failed: {self.lock.job} is running."
×
802
            cfg = RunCfg(
1✔
803
                func_name='take_iv',
804
                kwargs={'iv_kwargs': params['kwargs']},
805
                run_in_main_process=params['run_in_main_process'],
806
            )
807
            result = run_smurf_func(cfg)
1✔
808
            set_session_data(session, result)
1✔
809
            if result.success:
1✔
810
                block_data = {}
1✔
811
                for qd in result.return_val['quantiles'].values():
1✔
812
                    if isinstance(qd, dict):
1✔
813
                        qd = QuantileData(**qd)
×
814
                    block_data.update(qd.to_block_data())
1✔
815
                d = {
1✔
816
                    'timestamp': time.time(),
817
                    'block_name': 'iv_results',
818
                    'data': block_data
819
                }
820
                self.agent.publish_to_feed('iv_results', d)
1✔
821
            return result.success, "Finished taking IV"
1✔
822

823
    @ocs_agent.param('kwargs', default=None)
1✔
824
    @ocs_agent.param('rfrac_range', default=(0.2, 0.9))
1✔
825
    @ocs_agent.param('tag', default=None)
1✔
826
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
827
    def take_bias_steps(self, session, params):
1✔
828
        """take_bias_steps(kwargs=None, rfrac_range=(0.2, 0.9), tag=None, run_in_main_process=False)
829

830
        **Task** - Takes bias_steps and saves the output filepath to the
831
        session data object. See the `sodetlib bias step docs page
832
        <https://simons1.princeton.edu/docs/sodetlib/operations/bias_steps.html>`_
833
        for more information on bias steps and what kwargs can be passed in.
834

835
        Args
836
        ----
837
        kwargs : dict
838
            Additional kwargs to pass to ``take_bais_steps`` function.
839
        rfrac_range : tuple
840
            Range of valid rfracs to check against when determining the number
841
            of good detectors.
842
        tag : Optional[str]
843
            String containing a tag or comma-separated list of tags to attach
844
            to the g3 stream.
845
        run_in_main_process : bool
846
            If true, run smurf-function in main process. Mainly for the purpose
847
            of testing without the reactor running.
848

849
        Notes
850
        ------
851
        The following data will be written to the session.data object::
852

853
            >> response.session['data']
854
            {
855
                'filepath': Filepath of saved BiasStepAnalysis object
856
                'biased_total': Total number of detectors biased into rfrac_range
857
                'biased_per_bg': List containing number of biased detectors on each bias line
858
                'quantiles': {
859
                    'Rtes': Rtes quantiles,
860
                    'Rfrac': Rfrac quantiles,
861
                    'Si': Si quantiles,
862
                }
863
            }
864
        """
865

866
        if params['kwargs'] is None:
1✔
867
            params['kwargs'] = {}
1✔
868

869
        if params['tag'] is not None:
1✔
870
            params['kwargs']['g3_tag'] = params['tag']
×
871

872
        with self.lock.acquire_timeout(0, job='bias_steps') as acquired:
1✔
873
            if not acquired:
1✔
874
                return False, f"Operation failed: {self.lock.job} is running."
×
875

876
            cfg = RunCfg(
1✔
877
                func_name='take_bias_steps',
878
                kwargs={
879
                    'kwargs': params['kwargs'], 'rfrac_range': params['rfrac_range'],
880
                },
881
                run_in_main_process=params['run_in_main_process'],
882
            )
883
            result = run_smurf_func(cfg)
1✔
884
            set_session_data(session, result)
1✔
885
            if result.success:  # Publish quantile results
1✔
886
                block_data = {
1✔
887
                    f'biased_bg{bg}': v
888
                    for bg, v in enumerate(result.return_val['biased_per_bg'])
889
                }
890
                block_data['biased_total'] = result.return_val['biased_total']
1✔
891
                for qd in result.return_val['quantiles'].values():
1✔
892
                    if isinstance(qd, dict):
1✔
893
                        qd = QuantileData(**qd)
×
894
                    block_data.update(qd.to_block_data())
1✔
895
                data = {
1✔
896
                    'timestamp': time.time(),
897
                    'block_name': 'bias_steps_results',
898
                    'data': block_data
899
                }
900
                self.agent.publish_to_feed('bias_step_results', data)
1✔
901

902
            return result.success, "Finished taking bias steps"
1✔
903

904
    @ocs_agent.param('bgs', default=None)
1✔
905
    @ocs_agent.param('kwargs', default=None)
1✔
906
    @ocs_agent.param('tag', default=None)
1✔
907
    @ocs_agent.param('run_in_main_process', default=False, type=bool)
1✔
908
    def take_bias_waves(self, session, params):
1✔
909
        """take_bias_waves(kwargs=None, rfrac_range=(0.2, 0.9), tag=None, run_in_main_process=False)
910

911
        **Task** - Takes bias_wave and saves the output filepath to the
912
        session data object.
913

914
        Args
915
        ----
916
        kwargs : dict
917
            Additional kwargs to pass to ``take_bias_wave`` function.
918
        rfrac_range : tuple
919
            Range of valid rfracs to check against when determining the number
920
            of good detectors.
921
        tag : Optional[str]
922
            String containing a tag or comma-separated list of tags to attach
923
            to the g3 stream.
924
        run_in_main_process : bool
925
            If true, run smurf-function in main process. Mainly for the purpose
926
            of testing without the reactor running.
927

928
        Notes
929
        ------
930
        The following data will be written to the session.data object::
931

932
            >> response.session['data']
933
            {
934
                'filepath': Filepath of saved BiasWaveAnalysis object
935
                'biased_total': Total number of detectors biased into rfrac_range
936
                'biased_per_bg': List containing number of biased detectors on each bias line
937
                'quantiles': {
938
                    'Rtes': Rtes quantiles,
939
                    'Rfrac': Rfrac quantiles,
940
                    'Si': Si quantiles,
941
                }
942
            }
943
        """
944

945
        if params['kwargs'] is None:
×
946
            params['kwargs'] = {}
×
947

948
        if params['tag'] is not None:
×
949
            params['kwargs']['g3_tag'] = params['tag']
×
950

951
        with self.lock.acquire_timeout(0, job='bias_wave') as acquired:
×
952
            if not acquired:
×
953
                return False, f"Operation failed: {self.lock.job} is running."
×
954

955
            cfg = RunCfg(
×
956
                func_name='take_bias_waves',
957
                kwargs={
958
                    'kwargs': params['kwargs'], 'rfrac_range': params['rfrac_range'],
959
                },
960
                run_in_main_process=params['run_in_main_process'],
961
            )
962
            result = run_smurf_func(cfg)
×
963
            set_session_data(session, result)
×
964
            if result.success:  # Publish quantile results
×
965
                block_data = {
×
966
                    f'biased_bg{bg}': v
967
                    for bg, v in enumerate(result.return_val['biased_per_bg'])
968
                }
969
                block_data['biased_total'] = result.return_val['biased_total']
×
970
                for qd in result.return_val['quantiles'].values():
×
971
                    block_data.update(QuantileData(**qd).to_block_data())
×
972
                data = {
×
973
                    'timestamp': time.time(),
974
                    'block_name': 'bias_wave_results',
975
                    'data': block_data
976
                }
977
                self.agent.publish_to_feed('bias_wave_results', data)
×
978

979
            return result.success, "Finished taking bias steps"
×
980

981
    @ocs_agent.param('bgs', default=None)
1✔
982
    @ocs_agent.param('kwargs', default=None)
1✔
983
    def overbias_tes(self, session, params):
1✔
984
        """overbias_tes(bgs=None, kwargs=None)
985

986
        **Task** - Overbiases detectors using S.overbias_tes_all.
987

988
        Args
989
        -------
990
        bgs : List[int]
991
            List of bias groups to overbias. If this is set to None, it will
992
            use all active bgs.
993
        kwargs : dict
994
            Additional kwargs to pass to the ``overbias_tes_all`` function.
995
        """
996
        kw = {'bias_groups': params['bgs']}
1✔
997
        if params['kwargs'] is not None:
1✔
998
            kw.update(params['kwargs'])
×
999

1000
        with self.lock.acquire_timeout(0, job='overbias_tes') as acquired:
1✔
1001
            if not acquired:
1✔
1002
                return False, f"Operation failed: {self.lock.job} is running."
×
1003

1004
            S, cfg = self._get_smurf_control(session=session)
1✔
1005
            sdl.overbias_dets(S, cfg, **kw)
1✔
1006

1007
        return True, "Finished Overbiasing TES"
1✔
1008

1009
    @ocs_agent.param('bias')
1✔
1010
    @ocs_agent.param('bgs', default=None)
1✔
1011
    def set_biases(self, session, params):
1✔
1012
        """set_biases(bias, bgs=None)
1013

1014
        **Task** - Task used to set TES biases.
1015

1016
        Args
1017
        -----
1018
        bias: int, float, list
1019
            Biases to set. If a float is passed, this will be used for all
1020
            specified bgs. If a list of floats is passed, it must be the same
1021
            size of the list of bgs.
1022
        bgs: int, list, optional
1023
            Bias group (bg), or list of bgs to set. If None, will set all bgs.
1024
        """
1025
        if params['bgs'] is None:
×
1026
            bgs = np.arange(12)
×
1027
        else:
1028
            bgs = np.atleast_1d(params['bgs'])
×
1029

1030
        if isinstance(params['bias'], (int, float)):
×
1031
            biases = [params['bias'] for _ in bgs]
×
1032
        else:
1033
            if len(params['bias']) != len(bgs):
×
1034
                return False, "Number of biases must match number of bgs"
×
1035
            biases = params['bias']
×
1036

1037
        with self.lock.acquire_timeout(0, job='set_biases') as acquired:
×
1038
            if not acquired:
×
1039
                return False, f"Operation failed: {self.lock.job} is running."
×
1040

1041
            S, _ = self._get_smurf_control(session=session)
×
1042

1043
            for bg, bias in zip(bgs, biases):
×
1044
                S.set_tes_bias_bipolar(bg, bias)
×
1045

1046
            return True, f"Finished setting biases to {params['bias']}"
×
1047

1048
    @ocs_agent.param('bgs', default=None)
1✔
1049
    def zero_biases(self, session, params):
1✔
1050
        """zero_biases(bgs=None)
1051

1052
        **Task** - Zeros TES biases for specified bias groups.
1053

1054
        Args
1055
        -----
1056
        bgs: int, list, optional
1057
            bg, or list of bgs to zero. If None, will zero all bgs.
1058
        """
1059
        params['bias'] = 0
×
1060
        self.agent.start('set_biases', params)
×
1061
        self.agent.wait('set_biases')
×
1062
        return True, 'Finished zeroing biases'
×
1063

1064
    @ocs_agent.param('rfrac', default=(0.3, 0.6))
1✔
1065
    @ocs_agent.param('kwargs', default=None)
1✔
1066
    def bias_dets(self, session, params):
1✔
1067
        """bias_dets(rfrac=(0.3, 0.6), kwargs=None)
1068

1069
        **Task** - Biases detectors to a target Rfrac value or range. This
1070
        function uses IV results to determine voltages for each bias-group. If
1071
        rfrac is set to be a value, the bias voltage will be set such that the
1072
        median rfrac across all channels is as close as possible to the set
1073
        value. If a range is specified, the voltage will be chosen to maximize
1074
        the number of channels in that range.
1075

1076
        See the sodetlib docs page for `biasing dets into transition
1077
        <https://simons1.princeton.edu/docs/sodetlib/operations/iv.html#biasing-detectors-into-transition>`_
1078
        for more information on the functions and additional keyword args that
1079
        can be passed in.
1080

1081
        Args
1082
        -------
1083
        rfrac : float, tuple
1084
            Target rfrac range to aim for. If this is a float, bias voltages
1085
            will be chosen to get the median rfrac of each bias group as close
1086
            as possible to that value. If
1087
        kwargs : dict
1088
            Additional kwargs to pass to the ``bias_dets`` function.
1089

1090
        Notes
1091
        ------
1092
        The following data will be written to the session.data object::
1093

1094
            >> response.session['data']
1095
            {
1096
                'biases': List of voltage bias values for each bias-group
1097
            }
1098
        """
1099
        if params['kwargs'] is None:
1✔
1100
            params['kwargs'] = {}
×
1101

1102
        with self.lock.acquire_timeout(0, job='bias_steps') as acquired:
1✔
1103
            if not acquired:
1✔
1104
                return False, f"Operation failed: {self.lock.job} is running."
×
1105

1106
            S, cfg = self._get_smurf_control(session=session)
1✔
1107
            if isinstance(params['rfrac'], (int, float)):
1✔
1108
                biases = bias_dets.bias_to_rfrac(
×
1109
                    S, cfg, rfrac=params['rfrac'], **params['kwargs']
1110
                )
1111
            else:
1112
                biases = bias_dets.bias_to_rfrac_range(
1✔
1113
                    S, cfg, rfrac_range=params['rfrac'], **params['kwargs']
1114
                )
1115

1116
            session.data['biases'] = biases.tolist()
1✔
1117

1118
            return True, "Finished biasing detectors"
1✔
1119

1120
    @ocs_agent.param('disable_amps', default=True, type=bool)
1✔
1121
    @ocs_agent.param('disable_tones', default=True, type=bool)
1✔
1122
    def all_off(self, session, params):
1✔
1123
        """all_off(disable_amps=True, disable_tones=True)
1124

1125
        **Task** - Turns off tones, flux-ramp voltage and amplifier biases
1126

1127
        Args
1128
        -------
1129
        disable_amps: bool
1130
            If True, will disable amplifier biases
1131
        disable_tones: bool
1132
            If True, will turn off RF tones and flux-ramp signal
1133
        """
1134
        with self.lock.acquire_timeout(0, job='all_off') as acquired:
1✔
1135
            if not acquired:
1✔
1136
                return False, f"Operation failed: {self.lock.job} is running."
×
1137

1138
            S, cfg = self._get_smurf_control(session=session)
1✔
1139
            if params['disable_tones']:
1✔
1140
                S.all_off()
1✔
1141
            if params['disable_amps']:
1✔
1142
                S.C.write_ps_en(0)
1✔
1143

1144
            return True, "Everything off"
1✔
1145

1146
    @ocs_agent.param('_')
1✔
1147
    def restart_rssi(self, session, params):
1✔
1148
        """restart_rssi()
1149

1150
        **Task** - Restarts the RSSI. Use to recover from lock-up.
1151

1152
        Notes
1153
        ------
1154
        The following data will be written to the session.data object::
1155

1156
            >> response.session['data']
1157
            {
1158
                'rssi_responsive': Boolean indicating whether queries over RSSI succeed,
1159
                'last_updated': The time of the update,
1160
            }
1161
        """
1162
        with self.lock.acquire_timeout(0, job='restart_rssi') as acquired:
×
1163
            if not acquired:
×
1164
                return False, f"Operation failed: {self.lock.job} is running."
×
1165

1166
            # this register being unresponsive is a symptom of an rssi lock-up
NEW
1167
            slot_reg = f"smurf_server_s{self.slot}:AMCc:FpgaTopLevel:AmcCarrierCore:AmcCarrierBsi:SlotNumber"
×
1168

1169
            # if the system is locked up, spawning a SmurfControl instance will hang so we use epics
NEW
1170
            slot = epics.caget(slot_reg, timeout=2)
×
1171

1172
            # reset if RSSI is locked-up
NEW
1173
            success = True
×
NEW
1174
            msg = ""
×
NEW
1175
            if slot is not None:  # query suceeded
×
NEW
1176
                success, msg = True, "RSSI is not locked-up -- Doing nothing."
×
1177
            else:
1178
                # send the reset command
NEW
1179
                epics.caput(f"smurf_server_s{self.slot}:AMCc:RestartRssi", 1)
×
1180

1181
                # check the system has recovered
NEW
1182
                slot = epics.caget(slot_reg, timeout=2)
×
NEW
1183
                success = slot is not None
×
NEW
1184
                msg = "RSSI remains locked-up" if slot is None else "RSSI has recovered"
×
1185

1186
            # store state in the session data
NEW
1187
            session.data.update({
×
1188
                "rssi_responsive": slot is not None,
1189
                "last_updated": time.time()
1190
            })
1191

NEW
1192
            return success, msg
×
1193

1194

1195
def make_parser(parser=None):
1✔
1196
    """
1197
    Builds argsparse parser, allowing sphinx to auto-document it.
1198
    """
1199
    if parser is None:
1✔
1200
        parser = argparse.ArgumentParser()
1✔
1201

1202
    pgroup = parser.add_argument_group('Agent Options')
1✔
1203
    pgroup.add_argument('--monitor-id', '-m', type=str,
1✔
1204
                        help="Instance id for pysmurf-monitor corresponding to "
1205
                             "this pysmurf instance.")
1206
    pgroup.add_argument('--slot', type=int,
1✔
1207
                        help="Smurf slot that this agent will be controlling")
1208
    pgroup.add_argument('--poll-interval', type=float,
1✔
1209
                        help="Time between check-state polls")
1210
    return parser
1✔
1211

1212

1213
def main(args=None):
1✔
1214
    parser = make_parser()
×
1215
    args = site_config.parse_args(agent_class='PysmurfController',
×
1216
                                  parser=parser,
1217
                                  args=args)
1218

1219
    agent, runner = ocs_agent.init_site_agent(args)
×
1220
    controller = PysmurfController(agent, args)
×
1221

1222
    agent.register_task('run', controller.run, blocking=False)
×
1223
    agent.register_task('abort', controller.abort, blocking=False)
×
1224

1225
    startup_pars = {}
×
1226
    if args.poll_interval is not None:
×
1227
        startup_pars['poll_interval'] = args.poll_interval
×
1228
    agent.register_process(
×
1229
        'check_state', controller.check_state, controller._stop_check_state,
1230
        startup=startup_pars
1231
    )
1232
    agent.register_process(
×
1233
        'stream', controller.stream, controller._stream_stop
1234
    )
1235
    agent.register_task('uxm_setup', controller.uxm_setup)
×
1236
    agent.register_task('uxm_relock', controller.uxm_relock)
×
1237
    agent.register_task('take_bgmap', controller.take_bgmap)
×
1238
    agent.register_task('bias_dets', controller.bias_dets)
×
1239
    agent.register_task('take_iv', controller.take_iv)
×
1240
    agent.register_task('take_bias_steps', controller.take_bias_steps)
×
1241
    agent.register_task('take_bias_waves', controller.take_bias_waves)
×
1242
    agent.register_task('overbias_tes', controller.overbias_tes)
×
1243
    agent.register_task('take_noise', controller.take_noise)
×
1244
    agent.register_task('bias_dets', controller.bias_dets)
×
1245
    agent.register_task('all_off', controller.all_off)
×
1246
    agent.register_task('set_biases', controller.set_biases)
×
1247
    agent.register_task('zero_biases', controller.zero_biases)
×
1248
    agent.register_task('run_test_func', controller.run_test_func)
×
1249
    agent.register_task('restart_rssi', controller.restart_rssi)
×
1250

1251
    runner.run(agent, auto_reconnect=True)
×
1252

1253

1254
if __name__ == '__main__':
1✔
1255
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc