diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e5da254e..5e66ffa65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,10 +12,10 @@ repos: - id: isort name: isort (python) - repo: https://github.com/hhatto/autopep8 - rev: v2.2.0 + rev: v2.3.1 hooks: - id: autopep8 - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 diff --git a/docs/agents/wiregrid_tiltsensor.rst b/docs/agents/wiregrid_tiltsensor.rst new file mode 100644 index 000000000..23f0a0baa --- /dev/null +++ b/docs/agents/wiregrid_tiltsensor.rst @@ -0,0 +1,70 @@ +.. highlight:: rst + +.. _wiregrid_tiltsensor: + +========================== +Wiregrid Tilt Sensor Agent +========================== + +The Wiregrid Tilt Sensor Agent records the wire-grid tilt sensor outputs +related to the tilt angle of the wire-grid plane along the gravitaional direction. +There is two types of tilt sensors, DWL and sherborne. +The tilt sensor data is sent via serial-to-ethernet converter. +The converter is linked to the tilt sensor +via RS-422(DWL) or RS-485(sherborne), D-sub 9-pin cable. +The agent communicates with the converter via Ethernet. + +.. argparse:: + :filename: ../socs/agents/wiregrid_tiltsensor/agent.py + :func: make_parser + :prog: python3 agent.py + +Configuration File Examples +--------------------------- + +Below are configuration examples for the ocs config file and for running the +Agent in a docker container. + +OCS Site Config +```````````````` + +An example site-config-file block:: + + {'agent-class': 'WiregridTiltSensorAgent', + 'instance-id': 'wg-tilt-sensor', + 'arguments': ['--ip-address', '192.168.11.27', + '--port', '32', + '--sensor-type', 'DWL']}, + +- ``ip-address`` is an IP address of the serial-to-ethernet converter. +- ``port`` is an asigned port for the tilt sensor. + (The converter has four D-sub ports, 23, 26, 29, 32, to control + multiple devices connected via serial communication. + Communicating device is determined by the ethernet port number of the converter.) +- ``sensor_type`` represents the type of tilt sensor to communicate with. + We have the two types of tilt sensor, DWL and sherborne. + Available values of this argument are only 'DWL' or 'sherborne', + and depend on SATp. + +Docker Compose +`````````````` + +An example docker-compose configuration:: + + ocs-wg-tilt-sensor-agent: + image: simonsobs/socs:latest + hostname: ocs-docker + network_mode: "host" + command: + - INSTANCE_ID=wg-tilt-sensor + volumes: + - ${OCS_CONFIG_DIR}:/config:ro + +- Since the agent within the container needs to communicate with hardware on the + host network you must use ``network_mode: "host"`` in your compose file. + +Agent API +--------- + +.. autoclass:: socs.agents.wiregrid_tiltsensor.agent.WiregridTiltSensorAgent + :members: diff --git a/docs/index.rst b/docs/index.rst index 87f62e4c0..2f1a8eda2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,6 +82,7 @@ API Reference Full API documentation for core parts of the SOCS library. agents/wiregrid_actuator agents/wiregrid_encoder agents/wiregrid_kikusui + agents/wiregrid_tiltsensor .. toctree:: :caption: Simulator Reference diff --git a/docs/requirements.txt b/docs/requirements.txt index 4609b7f8b..72ec8cc8e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx>=5.0.0 sphinx_rtd_theme>=2.0.0 -sphinx-argparse>=0.4.0 +# unpin when https://github.com/sphinx-doc/sphinx-argparse/issues/56 is fixed +sphinx-argparse==0.4.0 diff --git a/socs/agents/hi6200/__init__.py b/socs/agents/hi6200/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/socs/agents/hwp_supervisor/agent.py b/socs/agents/hwp_supervisor/agent.py index e03324ce4..3f3f346b5 100644 --- a/socs/agents/hwp_supervisor/agent.py +++ b/socs/agents/hwp_supervisor/agent.py @@ -118,12 +118,12 @@ class HWPClients: class IBootState: instance_id: str outlets: List[int] + agent_type: Literal['iboot, synaccess'] outlet_state: Dict[int, Optional[int]] = None op_data: Optional[Dict] = None def __post_init__(self): self.outlet_state = {o: None for o in self.outlets} - self.outlet_labels = {o: f'outletStatus_{o}' for o in self.outlets} def update(self): op = get_op_data(self.instance_id, 'acq', test_mode=False) @@ -132,10 +132,23 @@ def update(self): self.outlet_state = {o: None for o in self.outlets} return - self.outlet_state = { - outlet: op['data'][label]['status'] - for outlet, label in self.outlet_labels.items() - } + if self.agent_type == 'iboot': + self.outlet_labels = {o: f'outletStatus_{o}' for o in self.outlets} + self.outlet_state = { + outlet: op['data'][label]['status'] + for outlet, label in self.outlet_labels.items() + } + elif self.agent_type == 'synaccess': + self.outlet_labels = {o: str(o - 1) for o in self.outlets} + self.outlet_state = { + outlet: op['data']['fields'][label]['status'] + for outlet, label in self.outlet_labels.items() + } + else: + raise ValueError( + f"Invalid agent_type: {self.agent_type}. " + "Must be in ['iboot', 'synaccess']" + ) @dataclass @@ -238,13 +251,15 @@ def from_args(cls, args: argparse.Namespace): ) if args.gripper_iboot_id is not None: - self.gripper_iboot = IBootState(args.gripper_iboot_id, args.gripper_iboot_outlets) + self.gripper_iboot = IBootState(args.gripper_iboot_id, args.gripper_iboot_outlets, + args.gripper_power_agent_type) log.info("Gripper Ibootbar id set: {id}", id=args.gripper_iboot_id) else: log.warn("Gripper Ibootbar id not set") if args.driver_iboot_id is not None: - self.driver_iboot = IBootState(args.driver_iboot_id, args.driver_iboot_outlets) + self.driver_iboot = IBootState(args.driver_iboot_id, args.driver_iboot_outlets, + args.driver_power_agent_type) log.info("Driver Ibootbar id set: {id}", id=args.driver_iboot_id) else: log.warn("Driver Ibootbar id not set") @@ -284,6 +299,7 @@ def update_enc_state(self, op): """ self._update_from_keymap(op, { 'enc_freq': 'approx_hwp_freq', + 'encoder_last_updated': 'encoder_last_updated', 'last_quad': 'last_quad', 'last_quad_time': 'last_quad_time', }) @@ -1555,6 +1571,9 @@ def make_parser(parser=None): pgroup.add_argument( '--gripper-iboot-outlets', nargs='+', type=int, help="Outlets for gripper iboot power") + pgroup.add_argument( + '--gripper-power-agent-type', choices=['iboot', 'synaccess'], default=None, + help="Type of agent used for controlling the gripper power") pgroup.add_argument( '--acu-instance-id', diff --git a/socs/agents/ocs_plugin_so.py b/socs/agents/ocs_plugin_so.py index 977e5fe98..67b3e8850 100644 --- a/socs/agents/ocs_plugin_so.py +++ b/socs/agents/ocs_plugin_so.py @@ -54,5 +54,6 @@ ('WiregridActuatorAgent', 'wiregrid_actuator/agent.py'), ('WiregridEncoderAgent', 'wiregrid_encoder/agent.py'), ('WiregridKikusuiAgent', 'wiregrid_kikusui/agent.py'), + ('WiregridTiltSensorAgent', 'wiregrid_tiltsensor/agent.py'), ]: ocs.site_config.register_agent_class(n, os.path.join(root, f)) diff --git a/socs/agents/pysmurf_controller/agent.py b/socs/agents/pysmurf_controller/agent.py index e0f0bb308..b3d89c40f 100644 --- a/socs/agents/pysmurf_controller/agent.py +++ b/socs/agents/pysmurf_controller/agent.py @@ -24,7 +24,7 @@ from sodetlib.operations import bias_dets from socs.agents.pysmurf_controller.smurf_subprocess_util import ( - RunCfg, RunResult, run_smurf_func) + QuantileData, RunCfg, RunResult, run_smurf_func) class PysmurfScriptProtocol(protocol.ProcessProtocol): @@ -129,7 +129,10 @@ def __init__(self, agent, args): 'observatory.{}.feeds.pysmurf_session_data'.format(args.monitor_id), ) - self.agent.register_feed('bias_step_quantiles', record=True) + self.agent.register_feed('bias_step_results', record=True) + self.agent.register_feed('noise_results', record=True) + self.agent.register_feed('iv_results', record=True) + self.agent.register_feed('bias_wave_results', record=True) def _on_session_data(self, _data): data, feed = _data @@ -655,6 +658,18 @@ def take_noise(self, session, params): result = run_smurf_func(cfg) set_session_data(session, result) + if result.success: + block_data = {} + for qd in result.return_val['quantiles'].values(): + if isinstance(qd, dict): + qd = QuantileData(**qd) + block_data.update(qd.to_block_data()) + d = { + 'timestamp': time.time(), + 'block_name': 'noise_results', + 'data': block_data + } + self.agent.publish_to_feed('noise_results', d) return result.success, "Finished taking noise" @ocs_agent.param('kwargs', default=None) @@ -747,11 +762,11 @@ def take_iv(self, session, params): >> response.session['data'] { - 'bands': Bands number of each resonator - 'channels': Channel number of each resonator - 'bgmap': BGMap assignment for each resonator - 'R_n': Normal resistance for each resonator 'filepath': Filepath of saved IVAnalysis object + 'quantiles': { + 'Rn': Rn quantiles + 'p_sat': electrical power at 90% Rn quantiles + } } """ if params['kwargs'] is None: @@ -770,6 +785,18 @@ def take_iv(self, session, params): ) result = run_smurf_func(cfg) set_session_data(session, result) + if result.success: + block_data = {} + for qd in result.return_val['quantiles'].values(): + if isinstance(qd, dict): + qd = QuantileData(**qd) + block_data.update(qd.to_block_data()) + d = { + 'timestamp': time.time(), + 'block_name': 'iv_results', + 'data': block_data + } + self.agent.publish_to_feed('iv_results', d) return result.success, "Finished taking IV" @ocs_agent.param('kwargs', default=None) @@ -807,14 +834,11 @@ def take_bias_steps(self, session, params): 'filepath': Filepath of saved BiasStepAnalysis object 'biased_total': Total number of detectors biased into rfrac_range 'biased_per_bg': List containing number of biased detectors on each bias line - 'Rtes_quantiles': { - 'Rtes': List of 15%, 25%, 50%, 75%, 85% Rtes quantiles, - 'quantiles': List of quantile labels - 'count': Total count of the distribution + 'quantiles': { + 'Rtes': Rtes quantiles, + 'Rfrac': Rfrac quantiles, + 'Si': Si quantiles, } - 'responsivity_quantiles': Same as above for responsivity - 'Rfrac_quantiles': Same as above for Rfrac - } """ @@ -838,15 +862,21 @@ def take_bias_steps(self, session, params): result = run_smurf_func(cfg) set_session_data(session, result) if result.success: # Publish quantile results - for name, d in result.return_val['quantiles'].items(): - block = dict(zip(d['labels'], d['values'])) - block[f'{name}_count'] = d['count'] - pub_data = { - 'timestamp': time.time(), - 'block_name': f'{name}_quantile', - 'data': block - } - self.agent.publish_to_feed('bias_step_quantiles', pub_data) + block_data = { + f'biased_bg{bg}': v + for bg, v in enumerate(result.return_val['biased_per_bg']) + } + block_data['biased_total'] = result.return_val['biased_total'] + for qd in result.return_val['quantiles'].values(): + if isinstance(qd, dict): + qd = QuantileData(**qd) + block_data.update(qd.to_block_data()) + data = { + 'timestamp': time.time(), + 'block_name': 'bias_steps_results', + 'data': block_data + } + self.agent.publish_to_feed('bias_step_results', data) return result.success, "Finished taking bias steps" @@ -883,14 +913,11 @@ def take_bias_waves(self, session, params): 'filepath': Filepath of saved BiasWaveAnalysis object 'biased_total': Total number of detectors biased into rfrac_range 'biased_per_bg': List containing number of biased detectors on each bias line - 'Rtes_quantiles': { - 'Rtes': List of 15%, 25%, 50%, 75%, 85% Rtes quantiles, - 'quantiles': List of quantile labels - 'count': Total count of the distribution + 'quantiles': { + 'Rtes': Rtes quantiles, + 'Rfrac': Rfrac quantiles, + 'Si': Si quantiles, } - 'responsivity_quantiles': Same as above for responsivity - 'Rfrac_quantiles': Same as above for Rfrac - } """ @@ -914,15 +941,19 @@ def take_bias_waves(self, session, params): result = run_smurf_func(cfg) set_session_data(session, result) if result.success: # Publish quantile results - for name, d in result.return_val['quantiles'].items(): - block = dict(zip(d['labels'], d['values'])) - block[f'{name}_count'] = d['count'] - pub_data = { - 'timestamp': time.time(), - 'block_name': f'{name}_quantile', - 'data': block - } - self.agent.publish_to_feed('bias_wave_quantiles', pub_data) + block_data = { + f'biased_bg{bg}': v + for bg, v in enumerate(result.return_val['biased_per_bg']) + } + block_data['biased_total'] = result.return_val['biased_total'] + for qd in result.return_val['quantiles'].values(): + block_data.update(QuantileData(**qd).to_block_data()) + data = { + 'timestamp': time.time(), + 'block_name': 'bias_wave_results', + 'data': block_data + } + self.agent.publish_to_feed('bias_wave_results', data) return result.success, "Finished taking bias steps" diff --git a/socs/agents/pysmurf_controller/smurf_subprocess_util.py b/socs/agents/pysmurf_controller/smurf_subprocess_util.py index ab7214904..878b2c181 100644 --- a/socs/agents/pysmurf_controller/smurf_subprocess_util.py +++ b/socs/agents/pysmurf_controller/smurf_subprocess_util.py @@ -2,7 +2,7 @@ import os import sys import traceback -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass, field, is_dataclass from typing import Dict, List, Optional import numpy as np @@ -23,6 +23,8 @@ def json_safe(data): Python basic types. - Converting NaN/inf to Num (0 or +/- large number) """ + if is_dataclass(data): + return json_safe(asdict(data)) if isinstance(data, dict): return {k: json_safe(v) for k, v in data.items()} if isinstance(data, (list, tuple)): @@ -34,17 +36,50 @@ def json_safe(data): return data if isinstance(data, float): return np.nan_to_num(data) + if data is None: + return None + + print(f"Unsure how to encode data of type {type(data)}...") + print(f"data: {data}") # This could still be something weird but json.dumps will # probably reject it! return data +@dataclass +class QuantileData: + name: str + quantiles: List[float] + values: List[float] + total: int + + def to_block_data(self): + "Format quantile data for OCS HK block" + labels = [f'{self.name}_q{int(q)}' for q in self.quantiles] + block_data = dict(zip(labels, self.values)) + block_data[f'{self.name}_total'] = self.total + return block_data + + +def compute_quantiles(name: str, arr: np.ndarray, quantiles: List[float]): + """ + Computes QuantileData object for a given array + + """ + quantiles = [float(q) for q in quantiles] + if np.isnan(arr).all(): + return QuantileData(name, quantiles, [0 for _ in quantiles], 0) + qs = [float(np.nan_to_num(np.nanquantile(arr, q / 100))) for q in quantiles] + total = int(np.sum(~np.isnan(arr))) + return QuantileData(name, quantiles, qs, total) + + def encode_dataclass(obj): """ Encodes a data-class into json, replacing any json-unsafe types with reasonable alternatives. """ - data = json_safe(asdict(obj)) + data = json_safe(obj) return json.dumps(data).encode() @@ -66,7 +101,14 @@ def take_noise(duration, kwargs=None): if kwargs is None: kwargs = {} S, cfg = get_smurf_control() - sdl.noise.take_noise(S, cfg, duration, **kwargs) + _, res = sdl.noise.take_noise(S, cfg, duration, **kwargs) + wls = res['noise_pars'][:, 0] + qs = [10, 25, 50, 75, 90] + return { + 'quantiles': { + 'white_noise_level': compute_quantiles('white_noise_level', wls, qs), + } + } def take_bgmap(kwargs=None): @@ -91,7 +133,14 @@ def take_iv(iv_kwargs=None): if iv_kwargs is None: iv_kwargs = {} iva = iv.take_iv(S, cfg, **iv_kwargs) - return {'filepath': iva.filepath} + quantiles = [10, 25, 50, 75, 90] + return { + 'filepath': iva.filepath, + 'quantiles': { + 'Rn': compute_quantiles('Rn', iva.R_n, quantiles), + 'p_sat': compute_quantiles('p_sat', iva.p_sat, quantiles) + } + } def run_uxm_setup(bands=None, kwargs=None): @@ -112,28 +161,6 @@ def run_uxm_relock(bands=None, kwargs=None): return None -def _process_quantiles(quantiles: list, arrays: Dict): - """ - Args - ---- - quantiles: list of quantiles to compute (in percent) - arrays: Dict of arrays to compute quantiles for - """ - res = {} - for name, arr in arrays.items(): - if np.isnan(arr).all(): - continue - labels = [f'{name}_q{q}' for q in quantiles] - qs = [float(np.nan_to_num(np.nanquantile(arr, q / 100))) for q in quantiles] - count = int(np.sum(~np.isnan(arr))) - res[name] = { - 'values': qs, - 'labels': labels, - 'count': count, - } - return res - - def take_bias_steps(kwargs=None, rfrac_range=(0.2, 0.9)): """Takes bias steps and computes quantiles for various parameters""" if kwargs is None: @@ -146,18 +173,19 @@ def take_bias_steps(kwargs=None, rfrac_range=(0.2, 0.9)): rfrac_range[0] < bsa.Rfrac, rfrac_range[1] > bsa.Rfrac ]) + qs = [10, 25, 50, 75, 90] data = { 'filepath': bsa.filepath, 'biased_total': int(np.sum(biased)), 'biased_per_bg': [ int(np.sum(biased[bsa.bgmap == bg])) for bg in range(12) ], + 'quantiles': { + 'Rfrac': compute_quantiles('Rfrac', bsa.Rfrac, qs), + 'Si': compute_quantiles('Si', bsa.Si, qs), + 'Rtes': compute_quantiles('Rtes', bsa.R0, qs), + } } - arrays = { - 'Rfrac': bsa.Rfrac, 'responsivity': bsa.Si, 'Rtes': bsa.R0, - } - quantiles = np.array([15, 25, 50, 75, 85]) - data['quantiles'] = _process_quantiles(quantiles, arrays) return data @@ -172,18 +200,19 @@ def take_bias_waves(kwargs=None, rfrac_range=(0.2, 0.9)): rfrac_range[0] < bwa.Rfrac, rfrac_range[1] > bwa.Rfrac ]) + qs = [10, 25, 50, 75, 90] data = { 'filepath': bwa.filepath, 'biased_total': int(np.sum(biased)), 'biased_per_bg': [ int(np.sum(biased[bwa.bgmap == bg])) for bg in range(12) ], + 'quantiles': { + 'Rfrac': compute_quantiles('Rfrac', bwa.Rfrac, qs), + 'Si': compute_quantiles('Si', bwa.Si, qs), + 'Rtes': compute_quantiles('Rtes', bwa.R0, qs), + } } - arrays = { - 'Rfrac': bwa.Rfrac, 'responsivity': bwa.Si, 'Rtes': bwa.R0, - } - quantiles = np.array([15, 25, 50, 75, 85]) - data['quantiles'] = _process_quantiles(quantiles, arrays) return data diff --git a/socs/agents/wiregrid_kikusui/agent.py b/socs/agents/wiregrid_kikusui/agent.py index 22988fb65..d5af74c59 100644 --- a/socs/agents/wiregrid_kikusui/agent.py +++ b/socs/agents/wiregrid_kikusui/agent.py @@ -289,14 +289,14 @@ def set_off(self, session, params=None): return True, 'Set Kikusui off' @ocs_agent.param('current', default=0., type=float, - check=lambda x: 0.0 <= x <= 4.5) + check=lambda x: 0.0 <= x <= 4.9) def set_c(self, session, params): """set_c(current=0) **Task** - Set current [A] Parameters: - current (float): set current [A] (should be [0.0, 4.5]) + current (float): set current [A] (should be [0.0, 4.9]) """ current = params.get('current') diff --git a/socs/agents/wiregrid_tiltsensor/__init__.py b/socs/agents/wiregrid_tiltsensor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/socs/agents/wiregrid_tiltsensor/agent.py b/socs/agents/wiregrid_tiltsensor/agent.py new file mode 100644 index 000000000..e54b8a930 --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/agent.py @@ -0,0 +1,218 @@ +import time + +from ocs import ocs_agent, site_config +from ocs.ocs_twisted import Pacemaker, TimeoutLock + +from socs.agents.wiregrid_tiltsensor.drivers import connect + + +class WiregridTiltSensorAgent: + """ Agent to record the wiregrid tilt sensor data. + The tilt sensor data is sent via serial-to-ethernet converter. + + Args: + ip (str): IP address of the serial-to-ethernet converter + port (int or str): Asigned port for the tilt sensor + The converter has four D-sub ports to control + multiple devices is determined + by the ethernet port number of converter. + sensor_type (str): Type of tilt sensor + There are twp types of tilt sensor, + and this argument is used for specifying + to communicate with whichtilt sensor. + This argument should be 'DWL' or 'sherborne'. + """ + + def __init__(self, agent, ip, port, sensor_type=None): + self.agent: ocs_agent.OCSAgent = agent + self.log = agent.log + self.lock = TimeoutLock() + + self.take_data = False + + self.ip = ip + self.port = port + self.sensor_type = sensor_type + self.tiltsensor = connect(self.ip, self.port, self.sensor_type) + self.pm = Pacemaker(2, quantize=True) + + agg_params = {'frame_length': 60} + self.agent.register_feed('wgtiltsensor', + record=True, + agg_params=agg_params, + buffer_time=1.) + + def acq(self, session, params=None): + """acq() + + **Process** - Run data acquisition. + + Notes: + The most recent data collected is stored in session.data in the + structure:: + + >>> response.session['data'] + {'tiltsensor_data': { + 'angleX': the angle in X-axis of tilt sensor, + 'angleY': the angle in Y-axis of tilt sensor, + 'temperatureX': the temperature in X-axis of tilt sensor + this is available for only sherborne, + 'temperatureY': the temperature in Y-axis of tilt sensor + this is available for only sherborne + }, + 'timestamp': timestamp when it updates tilt sensor data + } + """ + + with self.lock.acquire_timeout(timeout=0, job='acq') as acquired: + if not acquired: + self.log.warn( + 'Could not start acq because {} is already running' + ) + + # Initialize a take_data flag + self.take_data = True + last_release = time.time() + + tiltsensor_data = { + 'timestamp': 0, + 'block_name': 'wgtiltsensor', + 'data': { + 'angleX': -999, + 'angleY': -999, + 'temperatureX': -999, + 'temperatureY': -999, + }, + } + + self.log.info("Starting the count!") + + # Loop + while self.take_data: + # About every second, release and acquire the lock + if time.time() - last_release > 1.: + last_release = time.time() + if not self.lock.release_and_acquire(timeout=10): + print(f"Could not re-acquire lock now held by {self.lock.job}.") + return False + + # data taking + current_time = time.time() + msg, angles = self.tiltsensor.get_angle() + if self.sensor_type == 'sherborne': + msg, temperatures = self.tiltsensor.get_temp() + + tiltsensor_data['timestamp'] = current_time + tiltsensor_data['data']['angleX'] = angles[0] + tiltsensor_data['data']['angleY'] = angles[1] + if self.sensor_type == 'sherborne': + tiltsensor_data['data']['temperatureX'] = temperatures[0] + tiltsensor_data['data']['temperatureY'] = temperatures[1] + + self.agent.publish_to_feed('wgtiltsensor', tiltsensor_data) + + # store the session data + session.data = { + 'tiltsensor_data': { + 'angleX': tiltsensor_data['data']['angleX'], + 'angleY': tiltsensor_data['data']['angleY'], + 'temperatureX': tiltsensor_data['data']['temperatureX'], + 'temperatureY': tiltsensor_data['data']['temperatureY'] + }, + 'timestamp': current_time + } + + self.pm.sleep() # DAQ interval + # End of loop + # End of lock acquiring + + self.agent.feeds['wgtiltsensor'].flush_buffer() + + return True, 'Acquisition exited cleanly.' + + def stop_acq(self, session, params=None): + if self.take_data: + self.take_data = False + return True, 'requested to stop takeing data.' + else: + return False, 'acq is not currently running.' + + def reset(self, session, params=None): + """reset() + + **Task** - Reset the tiltsensor if the type of tiltsensor is sherborne. + + Notes: + The most recent data collected is stored in session.data in the + structure:: + + >>> response.session['data'] + {'reset': bool whether the reset successful or not + 'timestamp': timestamp when this command is performed + } + """ + + with self.lock.acquire_timeout(timeout=3.0, job='reset') as acquired: + if not acquired: + self.log.warn("Lock could not be acquired because it " + + f"is held by {self.lock.job}") + return False + + if self.sensor_type != 'sherborne': + return False, "This type of tiltsensor cannot reset." + else: + # Log the text provided to the Agent logs + self.log.info("running reset") + # Execute reset() + self.tiltsensor.reset() + # Store the timestamp when reset is performed in session.data + session.data = {'reset': True, + 'timestamp': time.time()} + + # True if task succeeds, False if not + return True, 'Reset the tiltsensor' + + +def make_parser(parser_in=None): + if parser_in is None: + import argparse + parser_in = argparse.ArgumentParser() + + pgroup = parser_in.add_argument_group('Agent Options') + pgroup.add_argument('--ip-address', dest='ip', type=str, default=None, + help='The ip adress of the serial-to-ethernet converter') + pgroup.add_argument('--port', dest='port', type=str, default=None, + help='The assigned port of the serial-to-ethernet converter ' + 'for the tilt sensor') + pgroup.add_argument('--sensor-type', + dest='sensor_type', + type=str, default=None, + help='The type of tilt sensor ' + 'running wiregrid tilt sensor DAQ') + return parser_in + + +def main(args=None): + parser_in = make_parser() + args = site_config.parse_args(agent_class='WiregridTiltSensorAgent', + parser=parser_in, + args=args) + + agent, runner = ocs_agent.init_site_agent(args) + + tiltsensor_agent = WiregridTiltSensorAgent(agent, + ip=args.ip, + port=args.port, + sensor_type=args.sensor_type) + + agent.register_process('acq', + tiltsensor_agent.acq, + tiltsensor_agent.stop_acq, + startup=True) + agent.register_task('reset', tiltsensor_agent.reset) + + runner.run(agent, auto_reconnect=True) + + +if __name__ == '__main__': + main() diff --git a/socs/agents/wiregrid_tiltsensor/drivers/__init__.py b/socs/agents/wiregrid_tiltsensor/drivers/__init__.py new file mode 100644 index 000000000..0cd09c22c --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/drivers/__init__.py @@ -0,0 +1,14 @@ +# DWL drivers +from socs.agents.wiregrid_tiltsensor.drivers.dwl import DWL +# sherborne drivers +from socs.agents.wiregrid_tiltsensor.drivers.sherborne import Sherborne + + +def connect(ip, port, sensor_type): + if sensor_type == 'DWL': + tiltsensor = DWL(tcp_ip=ip, tcp_port=port, timeout=0.5, isSingle=False, verbose=0) + elif sensor_type == 'sherborne': + tiltsensor = Sherborne(tcp_ip=ip, tcp_port=port, reset_boot=False, timeout=0.5, verbose=0) + else: + raise ('Invalid tiltsensor type') + return tiltsensor diff --git a/socs/agents/wiregrid_tiltsensor/drivers/dwl.py b/socs/agents/wiregrid_tiltsensor/drivers/dwl.py new file mode 100755 index 000000000..90783dd8e --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/drivers/dwl.py @@ -0,0 +1,153 @@ +# Built-in python modules +import time as tm + +# Control modules +from socs.common import moxa_serial as mx + + +class DWL: + """ + The DWL object is for communicating with the DWL-5000XY gravity sensor + + Args: + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + waittime = 0.05 # sec + + def __init__(self, tcp_ip=None, tcp_port=None, timeout=None, isSingle=False, verbose=0): + self.tcp_ip = tcp_ip + self.tcp_port = tcp_port + self.isSingle = isSingle + self.verbose = verbose + + # Connect to device + msg = self.__conn(tcp_ip, tcp_port, timeout) + print(msg) + + def __del__(self): + print(f"Disconnecting from TCP IP {self.tcp_ip} at port {self.tcp_port}") + self.ser.close() + return + + def get_angle(self): + """ Measure the single-axis or two-axis angle """ + self.clean_serial() + if self.isSingle: + command = b"\x06\x01\x01\xAA\x00\x00\x00\x00\x00\x00\x00\x00" + else: + command = b"\x06\x01\x02\xAA\x00\x00\x00\x00\x00\x00\x00\x00" + if self.verbose > 0: + print(f'get_angle() command = {command}') + + read = [] + SIZE = 12 + + # write and read serial + self.ser.write(command) + read_hex = self.ser.read(SIZE) + read = [hex(r) for r in read_hex] + if self.verbose > 0: + print(f'read_hex = {read_hex}') + print(f'read = {read}') + + # check the size of the string read + if not len(read) == SIZE: + msg = 'The size of the string read does not match with the expected size 12.' + if self.isSingle: + val = (-999) + else: + val = (-999, 999) + return msg, val + + # check header matching and calculate the angles + if self.isSingle: + header = ['0x61', '0x11'] + else: + header = ['0x61', '0x22'] + if read[0:2] == header: + readInt = [] + val = () + for c in read: + readInt.append((int)(c, 16)) + if self.isSingle: + nums = [readInt[5], readInt[4], readInt[3], readInt[2]] + angleX = (nums[0] << 24) + (nums[1] << 16) + (nums[2] << 8) + (nums[3]) + angleX = (angleX - 1800000) / 10000. + val = (angleX) + msg = f"Measured angle (1-axis) = {val}" + if self.verbose > 0: + print(readInt) + print(nums) + print((nums[1] << 16) / 1e+4, (nums[2] << 8) / 1e+4, (nums[3]) / 1e+4) + print('angle X = {}'.format(angleX)) + else: + readInt1 = readInt[5:8] + readInt2 = readInt[2:5] + readInt11 = readInt1 + readInt12 = readInt2 + numsX = [readInt11[2], readInt11[1], readInt11[0]] + numsY = [readInt12[2], readInt12[1], readInt12[0]] + angleX = (numsX[0] << 16) + (numsX[1] << 8) + (numsX[2]) + angleX = (angleX - 300000) / 10000. + angleY = (numsY[0] << 16) + (numsY[1] << 8) + (numsY[2]) + angleY = (angleY - 300000) / 10000. + val = (angleX, angleY) + msg = f"Measured angle (2-axis) = {val}" + if self.verbose > 0: + print(readInt) + print('numsX', numsX) + print((numsX[0] << 16) / 1e+4, (numsX[1] << 8) / 1e+4, (numsX[2]) / 1e+4) + print('numsY', numsY) + print((numsY[0] << 16) / 1e+4, (numsY[1] << 8) / 1e+4, (numsY[2]) / 1e+4) + print('angle X = {angleX}') + print('angle Y = {angleY}') + else: + msg = 'header NOT matching' + if self.isSingle: + val = -999 + else: + val = (-999, -999) + if self.verbose > 0: + print(msg) + return msg, val + + # ***** Helper Methods ***** + def __conn(self, tcp_ip=None, tcp_port=None, timeout=None): + """ + Connect to the tilt sensor module + + Args: + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + if tcp_ip is None or tcp_port is None: + raise Exception( + "Aborted DWL._conn() due to no " + "TCP port specified") + elif tcp_ip is not None and tcp_port is not None: + self.ser = mx.Serial_TCPServer((tcp_ip, tcp_port), timeout) + self.tcp_ip = tcp_ip + self.tcp_port = int(tcp_port) + self.using_tcp = True + msg = "Connected to TCP IP %s at port %d" % (tcp_ip, tcp_port) + command = b"\x06\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # initialization command + if self.verbose > 0: + print('initialization command = {}'.format(command)) + self.ser.write(command) + self.wait() + else: + raise Exception( + "Aborted DWL._conn() due to unknown error") + return msg + + def wait(self): + """ Sleep """ + tm.sleep(self.waittime) + return True + + def clean_serial(self): + """ Flush the serial buffer """ + self.ser.flushInput() + self.wait() + return True diff --git a/socs/agents/wiregrid_tiltsensor/drivers/sherborne.py b/socs/agents/wiregrid_tiltsensor/drivers/sherborne.py new file mode 100755 index 000000000..6b2f8173a --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/drivers/sherborne.py @@ -0,0 +1,118 @@ +import time as tm + +# Control modules +from socs.common import moxa_serial as mx + + +class Sherborne: + """ + The sherborne object is for communicating with the sherborne tilt sensor + + Args: + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + waittime = 0.05 # sec + waittime_reset = 30 # sec, this is the time for the sensor to reset. should be over 30 sec + address_Xaxis = 148 # default address of Xaxis is 148. this has individual sensor difference. + address_Yaxis = 149 # default address of Yaxis is 149. this has individual sensor difference. + + command_angleX = b'!' + str(address_Xaxis).encode('ascii') + b':SYS?\r' + command_angleY = b'!' + str(address_Yaxis).encode('ascii') + b':SYS?\r' + command_resetX = b'!' + str(address_Xaxis).encode('ascii') + b':RST\r' + command_resetY = b'!' + str(address_Yaxis).encode('ascii') + b':RST\r' + + def __init__(self, tcp_ip=None, tcp_port=None, timeout=None, reset_boot=False, verbose=0): + self.tcp_ip = tcp_ip + self.tcp_port = tcp_port + self.verbose = verbose + + # Connect to device + msg = self.__conn(tcp_ip, tcp_port, timeout) + print(msg) + if reset_boot: + self.reset() + + def __del__(self): + print(f"Disconnecting from TCP IP {self.tcp_ip} at port {self.tcp_port}") + self.ser.close() + return + + def get_angle(self): + """ Measure the two-axis angle """ + self.clean_serial() + if self.verbose > 0: + print(f'get_angle() commands = {self.command_angleX}, {self.command_angleY}') + SIZE = 16 + self.ser.write(self.command_angleX) + read_angleX = self.ser.read(SIZE) + value_read_angleX = read_angleX.decode('ascii') + value_read_angleX = value_read_angleX.replace('\r', '') + if self.verbose > 0: + print(f'read_angleX = {value_read_angleX}') + self.ser.write(self.command_angleY) + read_angleY = self.ser.read(SIZE) + value_read_angleY = read_angleY.decode('ascii') + value_read_angleY = value_read_angleY.replace('\r', '') + if self.verbose > 0: + print(f'read_angleY = {value_read_angleY}') + + self.wait() + + val = (value_read_angleX, value_read_angleY) + msg = f"Measured angle: X = {value_read_angleX}, Y = {value_read_angleY}" + if self.verbose > 0: + print(msg) + + return msg, val + + def __conn(self, tcp_ip, tcp_port, timeout): + """ + Connect to the tilt sensor module + + Args: + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + if tcp_ip is None or tcp_port is None: + raise Exception( + "Aborted Sherborne._conn() due to no TCP IP or " + "TCP port specified") + elif tcp_ip is not None and tcp_port is not None: + self.ser = mx.Serial_TCPServer((tcp_ip, tcp_port), timeout) + self.tcp_ip = tcp_ip + self.tcp_port = int(tcp_port) + self.using_tcp = True + msg = f"Connected to TCP IP {tcp_ip} at port {tcp_port}" + else: + raise Exception( + "Aborted Sherborne._conn() due to unknown error") + return msg + + def wait(self): + """ Sleep """ + tm.sleep(self.waittime) + return True + + def wait_reset(self): + """ Sleep for the reset time """ + tm.sleep(self.waittime_reset) + return True + + def clean_serial(self): + """ Flush the serial buffer """ + self.ser.flushInput() + return True + + def reset(self): + """ reset the tilt sensor """ + self.ser.write(self.command_resetX) + readX = self.ser.read(2) + if self.verbose > 0: + print(readX) + self.ser.write(self.command_resetY) + readY = self.ser.read(2) + if self.verbose > 0: + print(readY) + self.wait_reset() + return True diff --git a/socs/plugin.py b/socs/plugin.py index f98fa864e..14a9a1d44 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -47,4 +47,5 @@ 'WiregridActuatorAgent': {'module': 'socs.agents.wiregrid_actuator.agent', 'entry_point': 'main'}, 'WiregridEncoderAgent': {'module': 'socs.agents.wiregrid_encoder.agent', 'entry_point': 'main'}, 'WiregridKikusuiAgent': {'module': 'socs.agents.wiregrid_kikusui.agent', 'entry_point': 'main'}, + 'WiregridTiltSensorAgent': {'module': 'socs.agents.wiregrid_tiltsensor.agent', 'entry_point': 'main'}, } diff --git a/tests/agents/test_pysmurf_controller_agent.py b/tests/agents/test_pysmurf_controller_agent.py index c2a64a26a..54b249e75 100644 --- a/tests/agents/test_pysmurf_controller_agent.py +++ b/tests/agents/test_pysmurf_controller_agent.py @@ -127,7 +127,7 @@ def mock_take_noise(S, cfg, acq_time, **kwargs): **Mock** - Mock take_noise() in sodetlib. """ am = mock.MagicMock() - outdict = {'noise_pars': 0, + outdict = {'noise_pars': np.zeros((10, 3), dtype=float), 'bands': 0, 'channels': 0, 'band_medians': 0,