diff --git a/docs/agents/wiregrid_tiltsensor.rst b/docs/agents/wiregrid_tiltsensor.rst new file mode 100644 index 000000000..f827b636e --- /dev/null +++ b/docs/agents/wiregrid_tiltsensor.rst @@ -0,0 +1,85 @@ +.. highlight:: rst + +.. _wiregrid_kikusui: + +======================= +Wiregrid Kikusui Agent +======================= + +The Wiregrid Tiltsensor 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-tiltsensor', + 'arguments': [['--tiltsensor-ip', '192.168.11.27'], + ['--tiltsensor-port', '32'], + ['--tiltsensor_type', 'DWL']]}, + +- tiltsensor-ip is an IP address of the serial-to-ethernet converter. +- tiltsensor-port is an asigned port for the tilt sensor. + (The converter has four D-sub ports to control + multiple devices connected via serial communication. + Communicating device is determined by the ethernet port number of the converter.) +- tiltsensor_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-wgtiltsensor-agent: + image: simonsobs/socs:latest + hostname: ocs-docker + network_mode: "host" + command: + - INSTANCE_ID=wg-tiltsensor + 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. + + +Description +----------- + +Functions +````````` + +The agent has only one function, ``reset()``. +This function can be available only for sherborne tilt sensor. + +**Reset Function** + - reset(): + Reset the sherborne tilt sensor. + + +Agent API +--------- + +.. autoclass:: socs.agents.wiregrid_tiltsensor.agent.WiregridTiltsensorAgent + :members: diff --git a/socs/agents/ocs_plugin_so.py b/socs/agents/ocs_plugin_so.py index 880e91590..77af9f9a1 100644 --- a/socs/agents/ocs_plugin_so.py +++ b/socs/agents/ocs_plugin_so.py @@ -53,5 +53,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/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..ef659faf6 --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/agent.py @@ -0,0 +1,230 @@ +import time + +from ocs import ocs_agent, site_config +from ocs.ocs_twisted import TimeoutLock + +# DWL drivers +from socs.agents.wiregrid_tiltsensor.drivers.dwl import DWL +# sherborne drivers +from socs.agents.wiregrid_tiltsensor.drivers.sherborne import Sherborne + + +def connect(tiltsensor_ip, tiltsensor_port, type_tiltsensor): + if type_tiltsensor == 'DWL': + tiltsensor = DWL(tcp_ip=tiltsensor_ip, tcp_port=tiltsensor_port, timeout=0.5, isSingle=False, verbose=0) + elif type_tiltsensor == 'sherborne': + tiltsensor = Sherborne(tcp_ip=tiltsensor_ip, tcp_port=tiltsensor_port, reset_boot=False, timeout=0.5, verbose=0) + else: + raise ('Invalid tiltsensor type') + return tiltsensor + + +class WiregridTiltsensorAgent: + """ Agent to record the wiregrid tilt sensor data. + The tilt sensor data is sent via serial-to-ethernet converter. + + Args: + tiltsensor_ip (str): IP address of the serial-to-ethernet converter + tiltsensor_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. + type_tiltsensor (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, tiltsensor_ip, tiltsensor_port, type_tiltsensor=None): + self.agent: ocs_agent.OCSAgent = agent + self.log = agent.log + self.lock = TimeoutLock() + + self.take_data = False + + self.tiltsensor_ip = tiltsensor_ip + self.tiltsensor_port = tiltsensor_port + self.type_tiltsensor = type_tiltsensor + self.tiltsensor = connect(self.tiltsensor_ip, self.tiltsensor_port, self.type_tiltsensor) + + 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' + .format(self.lock.job)) + return False, 'Could not acquire lock.' + + session.set_status('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() + read_status, msg, angles = self.tiltsensor.get_angle() + if self.type_tiltsensor == 'sherborne': + msg, temperatures = self.tiltsensor.get_temp() + else: + pass + + tiltsensor_data['timestamp'] = current_time + tiltsensor_data['data']['angleX'] = angles[0] + tiltsensor_data['data']['angleY'] = angles[1] + if self.type_tiltsensor == 'sherborne': + tiltsensor_data['data']['temperatureX'] = temperatures[0] + tiltsensor_data['data']['temperatureY'] = temperatures[1] + else: + pass + + """ + with open('time.log', 'a') as f: + f.write(str(last_release) + '\n') + pass + """ + # self.log.info(f"sens_data:{sens_data}") + 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 + } + + time.sleep(0.5) # 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. + + """ + 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.type_tiltsensor != 'sherborne': + raise ("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('--tiltsensor_ip') + pgroup.add_argument('--tiltsensor_port') + pgroup.add_argument('--type_tiltsensor', + dest='type_tiltsensor', + 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, + tiltsensor_ip=args.type_tiltsensor, + tiltsensor_port=args.type_tiltsensor, + type_tiltsensor=args.type_tiltsensor) + + 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..e69de29bb diff --git a/socs/agents/wiregrid_tiltsensor/drivers/dwl.py b/socs/agents/wiregrid_tiltsensor/drivers/dwl.py new file mode 100755 index 000000000..1acabc0b1 --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/drivers/dwl.py @@ -0,0 +1,235 @@ +# Built-in python modules +import time as tm + +import serial as sr + +# 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: + rtu_port (str): Serial RTU port + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + waittime = 0.05 # sec + + def __init__(self, rtu_port=None, tcp_ip=None, tcp_port=None, timeout=None, isSingle=False, verbose=0): + self.isSingle = isSingle + self.verbose = verbose + if rtu_port is not None: + self.using_rtu = True + self.using_tcp = None + if tcp_ip is not None: + self.using_rtu = None + self.using_tcp = True + + # Connect to device + msg = self.__conn(rtu_port, tcp_ip, tcp_port, timeout) + print(msg) + + def __del__(self): + if not self.using_tcp: + print( + "Disconnecting from RTU port %s" + % (self.rtu_port)) + else: + print( + "Disconnecting from TCP IP %s at port %d" + % (self.tcp_ip, self.tcp_port)) + pass + self.ser.close() + return + + def get_angle(self): + """ Measure the single-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" + pass + if self.verbose > 0: + print(f'get_angle() command = {command}') + pass + self.ser.write(command) + self.wait() + + read = [] + SIZE = 12 + read_status = True + start_read_time = tm.time() + current_time = start_read_time + + while True: + # check timeout for header mismatching + current_time = tm.time() + if current_time - start_read_time > 1.0: + read_status = False + if self.verbose > 0: + print('the header of read value did not match!') + print('interrupt reading') + break + # read serial + read0 = self.ser.read(SIZE) + size0 = len(read0) + if self.verbose > 0: + print(f'{size0}: "{read0}"') + pass + if size0 > 0: + if self.verbose > 0: + print('read0 = {}'.format([hex(r) for r in read0])) + pass + add = [hex(r) for r in read0] + read += add + if self.verbose > 0: + print(read) + pass + pass + # check header matching + while len(read) > 0: + if self.isSingle: + header = ['0x61', '0x11'] + # header = ['0x61','0x12'] + else: + header = ['0x61', '0x22'] + pass + if read[0:2] == header: + break + else: + read = read[1:] + pass + pass + + if len(read) >= SIZE: + break + pass + + if read_status: + size = len(read) + if self.verbose > 0: + print(f'read (size:{size}) = {read}') + pass + + if size > 0: + readInt = [] + val = () + for c in read: + readInt.append((int)(c, 16)) + pass + 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) + 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)) + pass + 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) + 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}') + pass + pass + pass + + if self.isSingle: + msg = f"Measured angle (1-axis) = {val}" + else: + msg = f"Measured angle (2-axis) = {val}" + pass + if self.verbose > 0: + print(msg) + pass + return read_status, msg, val + else: + msg = 'reading time out' + val = (-999, -999) + return read_status, msg, val + + # ***** Helper Methods ***** + + def __conn(self, rtu_port=None, tcp_ip=None, tcp_port=None, timeout=None): + """ + Connect to the PMX module + + Args: + rtu_port (str): Serial RTU port + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + print(rtu_port) + if rtu_port is None and (tcp_ip is None or tcp_port is None): + raise Exception( + "Aborted PMX._conn() due to no RTU or " + "TCP port specified") + elif (rtu_port is not None + and (tcp_ip is not None or tcp_port is not None)): + raise Exception( + "Aborted PMX._conn() due to RTU and TCP port both being " + "specified. Can only have one or the other.") + elif rtu_port is not None: + self.ser = sr.Serial( + port=rtu_port, baudrate=115200, bytesize=8, + parity='N', stopbits=1, timeout=timeout) + self.using_tcp = False + msg = "Connected to RTU port %s" % (rtu_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)) + pass + self.ser.write(command) + 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)) + pass + self.ser.write(command) + self.wait() + else: + raise Exception( + "Aborted PMX._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() + + command = b"\x06\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # initialization command + self.ser.write(command) + 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..5713de37a --- /dev/null +++ b/socs/agents/wiregrid_tiltsensor/drivers/sherborne.py @@ -0,0 +1,139 @@ +import time as tm + +import serial as sr + +# Control modules +from socs.common import moxa_serial as mx + + +class Sherborne: + """ + The sherborne object is for communicating with the sherborne gravity sensor + + Args: + rtu_port (str): Serial RTU port + 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, rtu_port=None, tcp_ip=None, tcp_port=None, timeout=None, reset_boot=False, verbose=0): + self.verbose = verbose + # Connect to device + msg = self.__conn(rtu_port, tcp_ip, tcp_port, timeout) + print(msg) + if reset_boot: + self.reset() + + def __del__(self): + if not self.using_tcp: + print( + f"Disconnecting from RTU port {self._rtu_port}") + self.ser.close() + else: + print( + f"Disconnecting from TCP IP {self._tcp_ip} at port {self._tcp_port}") + pass + 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}') + pass + 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}') + pass + 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}') + pass + 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) + pass + return msg, val + + def __conn(self, rtu_port, tcp_ip, tcp_port, timeout): + """ + Connect to the PMX module + + Args: + rtu_port (str): Serial RTU port + tcp_ip (str): TCP IP address + tcp_port (int): TCP port + """ + if rtu_port is None and (tcp_ip is None or tcp_port is None): + raise Exception( + "Aborted PMX._conn() due to no RTU or " + "TCP port specified") + elif (rtu_port is not None + and (tcp_ip is not None or tcp_port is not None)): + raise Exception( + "Aborted PMX._conn() due to RTU and TCP port both being " + "specified. Can only have one or the other.") + elif rtu_port is not None: + self.ser = sr.Serial( + port=rtu_port, baudrate=115200, bytesize=8, + parity=None, stopbits=1, timeout=timeout) + self.rtu_port = rtu_port + self.using_tcp = False + msg = "Connected to RTU port %s" % (rtu_port) + 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) + else: + raise Exception( + "Aborted PMX._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 91b5296d1..1fee823b5 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -46,4 +46,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'}, }