diff --git a/docs/agents/pysmurf-controller.rst b/docs/agents/pysmurf-controller.rst index 8ff5c9ac5..d0bbbc864 100644 --- a/docs/agents/pysmurf-controller.rst +++ b/docs/agents/pysmurf-controller.rst @@ -33,6 +33,12 @@ These can be installed via pip: $ python -m pip install 'sodetlib @ git+https://github.com/simonsobs/sodetlib.git@master' $ python -m pip install 'sotodlib @ git+https://github.com/simonsobs/sotodlib.git@master' +Additionally, ``socs`` should be installed with the ``pysmurf`` group: + +.. code-block:: bash + + $ pip install -U socs[pysmurf] + Configuration File Examples ----------------------------------- diff --git a/docs/agents/wiregrid_actuator.rst b/docs/agents/wiregrid_actuator.rst index 5ece25bcd..ca7e0fe9e 100644 --- a/docs/agents/wiregrid_actuator.rst +++ b/docs/agents/wiregrid_actuator.rst @@ -103,7 +103,9 @@ The parameter details are here: - distance: Actuator moving distance [mm] (default: 10) - speedrate: Actuator speed rate [0.0, 5.0] (default: 0.2) - DO NOT use speedrate > 1.0 if el != 90 deg!! + +.. warning:: + DO NOT use ``speedrate > 1.0`` if ``el != 90 deg``! Hardware Configurations diff --git a/docs/user/installation.rst b/docs/user/installation.rst index ce1f0f580..c1406ebe5 100644 --- a/docs/user/installation.rst +++ b/docs/user/installation.rst @@ -30,6 +30,8 @@ The different groups, and the agents they provide dependencies for are: - Magpie Agent * - ``pfeiffer`` - Pfeiffer TC 400 Agent + * - ``pysmurf`` + - Pysmurf Controller Agent * - ``smurf_sim`` - SMuRF File Emulator, SMuRF Stream Simulator * - ``synacc`` diff --git a/requirements.txt b/requirements.txt index 1f74fe79b..f7b09b7c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,7 @@ pandas pfeiffer-vacuum-protocol==0.4 # pysmurf controller +pyepics pysmurf @ git+https://github.com/slaclab/pysmurf.git@main sodetlib @ git+https://github.com/simonsobs/sodetlib.git@master sotodlib @ git+https://github.com/simonsobs/sotodlib.git@master diff --git a/setup.py b/setup.py index 37b6f2274..82de72509 100644 --- a/setup.py +++ b/setup.py @@ -37,11 +37,12 @@ pfeiffer_deps = ['pfeiffer-vacuum-protocol==0.4'] # Pysmurf Controller Agent -# pysmurf_deps = [ -# 'pysmurf @ git+https://github.com/slaclab/pysmurf.git@main', -# 'sodetlib @ git+https://github.com/simonsobs/sodetlib.git@master', -# 'sotodlib @ git+https://github.com/simonsobs/sotodlib.git@master', -# ] +pysmurf_deps = [ + 'pyepics', + # 'pysmurf @ git+https://github.com/slaclab/pysmurf.git@main', + # 'sodetlib @ git+https://github.com/simonsobs/sodetlib.git@master', + # 'sotodlib @ git+https://github.com/simonsobs/sotodlib.git@master', +] # SMuRF File Emulator, SMuRF Stream Simulator smurf_sim_deps = ['so3g'] @@ -60,7 +61,7 @@ # Note: Not including the holograph deps, which are Python 3.8 only. Also not # including any dependencies with only direct references. all_deps = acu_deps + labjack_deps + magpie_deps + pfeiffer_deps + \ - smurf_sim_deps + synacc_deps + timing_master_deps + pysmurf_deps + smurf_sim_deps + synacc_deps + timing_master_deps all_deps = list(set(all_deps)) setup( @@ -119,7 +120,7 @@ 'labjack': labjack_deps, 'magpie': magpie_deps, 'pfeiffer': pfeiffer_deps, - # 'pysmurf': pysmurf_deps, + 'pysmurf': pysmurf_deps, 'smurf_sim': smurf_sim_deps, 'synacc': synacc_deps, 'timing_master': timing_master_deps, diff --git a/socs/agents/hwp_gripper/agent.py b/socs/agents/hwp_gripper/agent.py index 62eeaaad3..c0207ce65 100644 --- a/socs/agents/hwp_gripper/agent.py +++ b/socs/agents/hwp_gripper/agent.py @@ -381,8 +381,8 @@ def shutdown(self, session, params=None): self.shutdown_mode = True return True, 'Shutdown completed' - def grip_hwp(self, session, params=None): - """grip_hwp() + def grip(self, session, params=None): + """grip() **Task** - Series of commands to automatically grip the HWP. This will return grippers to their home position, then move them each @@ -442,7 +442,8 @@ def run_and_append(func, *args, **kwargs): # Reset alarms. If the warm-limit is hit, the alarm will be triggered # and return_dict['result'] will be True - return_dict = run_and_append(self.client.reset, job='grip', check_shutdown=check_shutdown) + return_dict = run_and_append(self.client.reset, job='grip', + check_shutdown=check_shutdown) if return_dict['result']: # If the warm-limit is hit, move the actuator outwards bit @@ -676,9 +677,7 @@ def main(args=None): args=args) agent, runner = ocs_agent.init_site_agent(args) - gripper_agent = HWPGripperAgent(agent, mcu_ip=args.mcu_ip, - control_port=args.control_port, - supervisor_id=args.supervisor_id) + gripper_agent = HWPGripperAgent(agent, args) agent.register_task('init_connection', gripper_agent.init_connection, startup=True) agent.register_process('monitor_state', gripper_agent.monitor_state, diff --git a/socs/agents/hwp_pid/agent.py b/socs/agents/hwp_pid/agent.py index 2c530f352..b8a871a61 100644 --- a/socs/agents/hwp_pid/agent.py +++ b/socs/agents/hwp_pid/agent.py @@ -295,13 +295,17 @@ def acq(self, session, params): data = {'timestamp': time.time(), 'block_name': 'HWPPID', 'data': {}} - current_freq = self.pid.get_freq() - target_freq = self.pid.get_target() - direction = self.pid.get_direction() - - data['data']['current_freq'] = current_freq - data['data']['target_freq'] = target_freq - data['data']['direction'] = direction + try: + current_freq = self.pid.get_freq() + target_freq = self.pid.get_target() + direction = self.pid.get_direction() + + data['data']['current_freq'] = current_freq + data['data']['target_freq'] = target_freq + data['data']['direction'] = direction + except BaseException: + time.sleep(1) + continue self.agent.publish_to_feed('hwppid', data) diff --git a/socs/agents/hwp_pid/drivers/pid_controller.py b/socs/agents/hwp_pid/drivers/pid_controller.py index 283bae0c0..60d7c874a 100644 --- a/socs/agents/hwp_pid/drivers/pid_controller.py +++ b/socs/agents/hwp_pid/drivers/pid_controller.py @@ -22,15 +22,17 @@ class PID: def __init__(self, ip, port, verb=False): self.verb = verb + self.ip = ip + self.port = port self.hex_freq = '00000' self.direction = None self.target = 0 # Need to setup connection before setting direction - self.conn = self._establish_connection(ip, int(port)) + self.conn = self._establish_connection(self.ip, int(self.port)) self.set_direction('0') @staticmethod - def _establish_connection(ip, port, timeout=5): + def _establish_connection(ip, port, timeout=2): """Connect to PID controller. Args: @@ -45,18 +47,17 @@ def _establish_connection(ip, port, timeout=5): """ conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.settimeout(timeout) # unit tests might fail on first connection attempt attempts = 3 for attempt in range(attempts): try: conn.connect((ip, port)) break - except ConnectionRefusedError: + except (ConnectionRefusedError, OSError): print(f"Failed to connect to device at {ip}:{port}") - print(f"Connection attempts remaining: {attempts-attempt-1}") - time.sleep(1) - conn.settimeout(timeout) - + else: + raise RuntimeError('Could not connect to PID controller') return conn @staticmethod @@ -259,20 +260,21 @@ def send_message(self, msg): str: Respnose from the controller. """ - self.conn.sendall((msg + '\r\n').encode()) - time.sleep(0.5) # Don't send messages too quickly for attempt in range(2): try: + self.conn.sendall((msg + '\r\n').encode()) + time.sleep(0.5) # Don't send messages too quickly data = self.conn.recv(4096).decode().strip() - break - except socket.timeout: + return data + except (socket.timeout, OSError): print("Caught timeout waiting for response from PID controller. " + "Trying again...") time.sleep(1) if attempt == 1: - raise RuntimeError( - 'Response from PID controller timed out.') - return data + print("Resetting connection") + self.conn.close() + self.conn = self._establish_connection(self.ip, int(self.port)) + return self.send_message(msg) def return_messages(self, msg): """Decode list of responses from PID controller and return useful diff --git a/socs/agents/hwp_pmx/agent.py b/socs/agents/hwp_pmx/agent.py index 3694dbb4c..76b41d9ff 100644 --- a/socs/agents/hwp_pmx/agent.py +++ b/socs/agents/hwp_pmx/agent.py @@ -281,26 +281,31 @@ def acq(self, session, params): 'block_name': 'hwppmx', 'data': {} } - msg, curr = self.dev.meas_current() - data['data']['current'] = curr - msg, volt = self.dev.meas_voltage() - data['data']['voltage'] = volt + try: + msg, curr = self.dev.meas_current() + data['data']['current'] = curr - msg, code = self.dev.check_error() - data['data']['err_code'] = code - data['data']['err_msg'] = msg + msg, volt = self.dev.meas_voltage() + data['data']['voltage'] = volt - prot_code = self.dev.check_prot() - if prot_code != 0: - self.prot = prot_code + msg, code = self.dev.check_error() + data['data']['err_code'] = code + data['data']['err_msg'] = msg - prot_msg = self.dev.get_prot_msg(self.prot) - data['data']['prot_code'] = self.prot - data['data']['prot_msg'] = prot_msg + prot_code = self.dev.check_prot() + if prot_code != 0: + self.prot = prot_code - msg, src = self.dev.check_source() - data['data']['source'] = src + prot_msg = self.dev.get_prot_msg(self.prot) + data['data']['prot_code'] = self.prot + data['data']['prot_msg'] = prot_msg + + msg, src = self.dev.check_source() + data['data']['source'] = src + except BaseException: + time.sleep(sleep_time) + continue self.agent.publish_to_feed('hwppmx', data) session.data = {'curr': curr, diff --git a/socs/agents/hwp_pmx/drivers/PMX_ethernet.py b/socs/agents/hwp_pmx/drivers/PMX_ethernet.py index ae92c1fba..872719c31 100644 --- a/socs/agents/hwp_pmx/drivers/PMX_ethernet.py +++ b/socs/agents/hwp_pmx/drivers/PMX_ethernet.py @@ -1,5 +1,5 @@ +import socket import time -from socket import AF_INET, SOCK_STREAM, socket protection_status_key = [ 'Over voltage', @@ -21,26 +21,53 @@ class PMX: """ def __init__(self, ip, port): - self.sock = socket(AF_INET, SOCK_STREAM) - self.sock.connect((ip, port)) - self.sock.settimeout(5) - + self.ip = ip + self.port = port self.wait_time = 0.01 self.buffer_size = 128 + self.conn = self._establish_connection(self.ip, int(self.port)) + + def _establish_connection(self, ip, port, timeout=2): + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.settimeout(timeout) + attempts = 3 + for attempt in range(attempts): + try: + conn.connect((ip, port)) + break + except (ConnectionRefusedError, OSError): + print(f"Failed to connect to device at {ip}:{port}") + else: + raise RuntimeError('Could not connect to PID controller') + return conn def close(self): - self.sock.close() - - def read(self): - return self.sock.recv(self.buffer_size).decode('utf-8') + self.conn.close() + + def send_message(self, msg, read=True): + for attempt in range(2): + try: + self.conn.sendall(msg) + time.sleep(0.5) + if read: + data = self.conn.recv(self.buffer_size).decode('utf-8') + return data + return + except (socket.timeout, OSError): + print("Caught timeout waiting for responce from PMX. Trying again...") + time.sleep(1) + if attempt == 1: + print("Resetting connection") + self.conn.close() + self.conn = self._establish_connection(self.ip, int(self.port)) + return self.send_message(msg, read=read) def wait(self): time.sleep(self.wait_time) def check_output(self): """ Return the output status """ - self.sock.sendall(b'output?\n') - val = int(self.read()) + val = int(self.send_message(b'output?\n')) msg = "Measured output state = " states = {0: 'OFF', 1: 'ON'} if val in states: @@ -51,8 +78,7 @@ def check_output(self): def check_error(self): """ Check oldest error from error queues. Error queues store up to 255 errors """ - self.sock.sendall(b':system:error?\n') - val = self.read() + val = self.send_message(b':system:error?\n') code, msg = val.split(',') code = int(code) msg = msg[1:-2] @@ -60,94 +86,87 @@ def check_error(self): def clear_alarm(self): """ Clear alarm """ - self.sock.sendall(b'output:protection:clear\n') + self.send_message(b'output:protection:clear\n', read=False) def turn_on(self): """ Turn the PMX on """ - self.sock.sendall(b'output 1\n') + self.send_message(b'output 1\n', read=False) self.wait() return self.check_output() def turn_off(self): """ Turn the PMX off """ - self.sock.sendall(b'output 0\n') + self.send_message(b'output 0\n', read=False) self.wait() return self.check_output() def check_current(self): """ Check the current setting """ - self.sock.sendall(b'curr?\n') - val = float(self.read()) + val = float(self.send_message(b'curr?\n')) msg = "Current setting = {:.3f} A".format(val) return msg, val def check_voltage(self): """ Check the voltage setting """ - self.sock.sendall(b'volt?\n') - val = float(self.read()) + val = float(self.send_message(b'volt?\n')) msg = "Voltage setting = {:.3f} V".format(val) return msg, val def meas_current(self): """ Measure the current """ - self.sock.sendall(b'meas:curr?\n') - val = float(self.read()) + val = float(self.send_message(b'meas:curr?\n')) msg = "Measured current = {:.3f} A".format(val) return msg, val def meas_voltage(self): """ Measure the voltage """ - self.sock.sendall(b'meas:volt?\n') - val = float(self.read()) + val = float(self.send_message(b'meas:volt?\n')) msg = "Measured voltage = {:.3f} V".format(val) return msg, val def set_current(self, curr): """ Set the current """ - self.sock.sendall(b'curr %a\n' % curr) + self.send_message(b'curr %a\n' % curr, read=False) self.wait() return self.check_current() def set_voltage(self, vol): """ Set the voltage """ - self.sock.sendall(b'volt %a\n' % vol) + self.send_message(b'volt %a\n' % vol, read=False) self.wait() return self.check_voltage() def check_source(self): """ Check the source of PMX """ - self.sock.sendall(b'volt:ext:sour?\n') - val = self.read() + val = self.send_message(b'volt:ext:sour?\n') msg = "Source: " + val return msg, val def use_external_voltage(self): """ Set PMX to use external voltage """ - self.sock.sendall(b'volt:ext:sour volt\n') + self.send_message(b'volt:ext:sour volt\n', read=False) self.wait() return self.check_source() def ign_external_voltage(self): """ Set PMX to ignore external voltage """ - self.sock.sendall(b'volt:ext:sour none\n') + self.send_message(b'volt:ext:sour none\n', read=False) self.wait() return self.check_source() def set_current_limit(self, curr_lim): """ Set the PMX current limit """ - self.sock.sendall(b'curr:prot %a\n' % curr_lim) + self.send_message(b'curr:prot %a\n' % curr_lim, read=False) self.wait() - self.sock.sendall(b'curr:prot?\n') - val = float(self.read()) + val = float(self.send_message(b'curr:prot?\n')) msg = "Current Limit: {:.3f} A".format(val) return msg def set_voltage_limit(self, vol_lim): """ Set the PMX voltage limit """ - self.sock.sendall(b'volt:prot %a\n' % vol_lim) + self.send_message(b'volt:prot %a\n' % vol_lim, read=False) self.wait() - self.sock.sendall(b'volt:prot?\n') - val = float(self.read()) + val = float(self.send_message(b'volt:prot?\n')) msg = "Voltage Limit: {:.3f} V".format(val) return msg @@ -156,8 +175,7 @@ def check_prot(self): Return: val (int): protection status code """ - self.sock.sendall(b'stat:ques?\n') - val = int(self.read()) + val = int(self.send_message(b'stat:ques?\n')) return val def get_prot_msg(self, val): diff --git a/socs/agents/pysmurf_controller/agent.py b/socs/agents/pysmurf_controller/agent.py index 5e7204550..0b62ba914 100644 --- a/socs/agents/pysmurf_controller/agent.py +++ b/socs/agents/pysmurf_controller/agent.py @@ -12,6 +12,7 @@ import time from typing import Optional +import epics import numpy as np import sodetlib as sdl from ocs import ocs_agent, site_config @@ -316,7 +317,7 @@ def check_state(self, session, params=None): stream_id=cfg.stream_id, ) session.data.update(d) - except RuntimeError: + except (RuntimeError, epics.ca.ChannelAccessGetFailure): self.log.warn("Could not connect to epics server! Waiting and " "then trying again") diff --git a/socs/agents/wiregrid_actuator/agent.py b/socs/agents/wiregrid_actuator/agent.py index b60d4de9c..05dd5d555 100644 --- a/socs/agents/wiregrid_actuator/agent.py +++ b/socs/agents/wiregrid_actuator/agent.py @@ -278,18 +278,24 @@ def _eject(self, main_distance=920, main_speedrate=1.0): @ocs_agent.param('speedrate', default=1.0, type=float, check=lambda x: 0.0 < x <= 5.0) + @ocs_agent.param('high_speed', default=False, type=bool) def insert(self, session, params=None): - """insert() + """insert(speedrate=1.0, high_speed=False) **Task** - Insert the wire-grid into the forebaffle interface above the SAT. Parameters: speedrate (float): Actuator speed rate [0.0, 5.0] (default: 1.0) - DO NOT use speedrate > 1.0 if el != 90 deg!! + DO NOT use ``speedrate > 1.0`` if ``el != 90 deg``! + high_speed (bool): If False, speedrate is limited to 1.0. Defaults + to False. """ # Get parameters speedrate = params.get('speedrate') + high_speed = params.get('high_speed') + if not high_speed: + speedrate = min(speedrate, 1.0) self.log.info('insert(): set speed rate = {}' .format(speedrate)) @@ -314,18 +320,24 @@ def insert(self, session, params=None): @ocs_agent.param('speedrate', default=1.0, type=float, check=lambda x: 0.0 < x <= 5.0) + @ocs_agent.param('high_speed', default=False, type=bool) def eject(self, session, params=None): - """eject() + """eject(speedrate=1.0, high_speed=False) **Task** - Eject the wire-grid from the forebaffle interface above the SAT. Parameters: speedrate (float): Actuator speed rate [0.0, 5.0] (default: 1.0) - DO NOT use speedrate > 1.0 if el != 90 deg!! + DO NOT use ``speedrate > 1.0`` if ``el != 90 deg``! + high_speed (bool): If False, speedrate is limited to 1.0. Defaults + to False. """ # Get parameters speedrate = params.get('speedrate') + high_speed = params.get('high_speed') + if not high_speed: + speedrate = min(speedrate, 1.0) self.log.info('eject(): set speed rate = {}' .format(speedrate)) @@ -472,8 +484,9 @@ def eject_homing(self, session, params=None): @ocs_agent.param('distance', default=10., type=float) @ocs_agent.param('speedrate', default=0.2, type=float, check=lambda x: 0.0 < x <= 5.0) + @ocs_agent.param('high_speed', default=False, type=bool) def insert_test(self, session, params): - """insert_test(distance=10, speedrate=0.1) + """insert_test(distance=10, speedrate=0.2, high_speed=False) **Task** - Insert slowly the wire-grid into the forebaffle interface above the SAT with a small distance. @@ -481,11 +494,16 @@ def insert_test(self, session, params): Parameters: distance (float): Actuator moving distance [mm] (default: 10) speedrate (float): Actuator speed rate [0.0, 5.0] (default: 0.2) - DO NOT use speedrate > 1.0 if el != 90 deg!! + DO NOT use ``speedrate > 1.0`` if ``el != 90 deg``! + high_speed (bool): If False, speedrate is limited to 1.0. Defaults + to False. """ # Get parameters distance = params.get('distance') speedrate = params.get('speedrate') + high_speed = params.get('high_speed') + if not high_speed: + speedrate = min(speedrate, 1.0) self.log.info('insert_test(): set distance = {} mm' .format(distance)) self.log.info('insert_test(): set speed rate = {}' @@ -524,8 +542,9 @@ def insert_test(self, session, params): @ocs_agent.param('distance', default=10., type=float) @ocs_agent.param('speedrate', default=0.2, type=float, check=lambda x: 0.0 < x <= 5.0) + @ocs_agent.param('high_speed', default=False, type=bool) def eject_test(self, session, params): - """eject_test(distance=10, speedrate=0.1) + """eject_test(distance=10, speedrate=0.2, high_speed=False) **Task** - Eject slowly the wire-grid from the forebaffle interface above the SAT with a small distance. @@ -533,11 +552,16 @@ def eject_test(self, session, params): Parameters: distance (float): Actuator moving distance [mm] (default: 10) speedrate (float): Actuator speed rate [0.0, 5.0] (default: 0.2) - DO NOT use speedrate > 1.0 if el != 90 deg!! + DO NOT use ``speedrate > 1.0`` if ``el != 90 deg``! + high_speed (bool): If False, speedrate is limited to 1.0. Defaults + to False. """ # Get parameters distance = params.get('distance', 10) speedrate = params.get('speedrate', 0.2) + high_speed = params.get('high_speed') + if not high_speed: + speedrate = min(speedrate, 1.0) self.log.info('eject_test(): set distance = {} mm' .format(distance)) self.log.info('eject_test(): set speed rate = {}' diff --git a/socs/agents/wiregrid_actuator/drivers/Actuator.py b/socs/agents/wiregrid_actuator/drivers/Actuator.py index 12265c048..ed5c23b44 100644 --- a/socs/agents/wiregrid_actuator/drivers/Actuator.py +++ b/socs/agents/wiregrid_actuator/drivers/Actuator.py @@ -212,12 +212,12 @@ def move(self, distance, speedrate=0.1): msg = 'Actuator:move(): WARNING!: Did NOT move due to STOP flag.' print(msg) return False - if speedrate < 0. or speedrate > 1.: + if speedrate < 0. or speedrate > 5.: print('Actuator:move(): WARNING!: ' - 'Speedrate should be between 0 and 1.') + 'Speedrate should be between 0 and 5.') print('Actuator:move(): WARNING!: ' - 'Speedrate is sed to 0.1.') - speedrate = 0.1 + 'Speedrate is set to 1.0.') + speedrate = 1.0 speed = \ int(speedrate * (self.speed_max - self.speed_min) + self.speed_min) # distance_count is an absolute value