From 95f8a6d3b7690dcbe1152705f6acd5f32f680b85 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 11 Oct 2019 15:34:38 +1100 Subject: [PATCH 1/7] threading example --- examples/Untitled.ipynb | 233 ++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 128 deletions(-) diff --git a/examples/Untitled.ipynb b/examples/Untitled.ipynb index be6c6a5..6c1be0c 100644 --- a/examples/Untitled.ipynb +++ b/examples/Untitled.ipynb @@ -2,216 +2,193 @@ "cells": [ { "cell_type": "code", - "execution_count": 15, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "from enum import Flag" + "import threading\n", + "import time" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "led_status = 0b000000000000000000" + "# Thread, Timer, Event and Lock. Those are the bits you'll need.\n", + "\n", + "# For the non-blocking moves the basic idea is that you write a function that contains the \n", + "# while loop that checks when it's time to stop the move, you create a Thread and pass it \n", + "# that function, start the move, start the Thread, then return. The Thread will run in the \n", + "# background and stop the move then at target. (edited)\n", + "\n", + "# You can use Events to do simple communication between threads, e.g. you can pass one to \n", + "# the dome movement thread and use it to stop the move early if the abort function is called." ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "led_lights_ind = {\n", - " 'power': 2,\n", - " 'comms': 3,\n", - " 'warn': 4,\n", - " 'input_1': 5,\n", - " 'input_2': 6,\n", - " 'input_3': 7,\n", - " 'relay_3_normally_closed': 8,\n", - " 'relay_3_normally_open': 9,\n", - " 'relay_2_normally_closed': 10,\n", - " 'relay_2_normally_open': 11,\n", - " 'relay_1_normally_closed': 12,\n", - " 'relay_1_normally_open': 13,\n", - " 'output_3': 14,\n", - " 'output_2': 15,\n", - " 'output_1': 16,\n", - " 'adc_3': 17,\n", - " 'adc_2': 18,\n", - " 'adc_1': 19\n", - "}" + "import threading\n", + "import time\n", + "\n", + "class FakeDome(object):\n", + " def __init__(self):\n", + " self._encoder_count = 0\n", + " self.abort_event = threading.Event()\n", + "\n", + " @property\n", + " def encoder_count(self):\n", + " return self._encoder_count\n", + "\n", + " def move(self, ticks=10):\n", + " # dummy move command that just moves us 10 ticks from current encoder position\n", + " target_count = self.encoder_count + ticks\n", + " # create the monitor thread that tracks completion of movement command\n", + " self.monitor = threading.Thread(target=self.move_condition, args=[target_count])\n", + " # create a thread to simulate ticks\n", + " self.simticks = threading.Thread(target=self.simulate_ticks, args=[ticks])\n", + " # start both threads\n", + " self.monitor.start()\n", + " self.simticks.start()\n", + " return\n", + " \n", + " def move_condition(self, target_count):\n", + " # record the start time, to check for timeout condition\n", + " start = time.time()\n", + " while self.encoder_count < target_count:\n", + " # check to see if abort_event is set and end move() if it is\n", + " if self.abort_event.is_set():\n", + " return\n", + " # short break to space ticks/status-checks out\n", + " time.sleep(1)\n", + " print(f'encoder count recorded, {target_count - self.encoder_count} ticks remaining.')\n", + " # if monitor thread has been going for more than 15s end it\n", + " if time.time() - start > 15:\n", + " return\n", + " return\n", + " \n", + " def simulate_ticks(self, ticks):\n", + " for tick in range(ticks):\n", + " # check to see if abort_event is set and end move() if it is\n", + " if self.abort_event.is_set():\n", + " return\n", + " time.sleep(1)\n", + " print(f'ENCODER COUNT INCREMENT, current count: {self.encoder_count}')\n", + " self._encoder_count+=1\n", + " return\n", + " \n", + " def abort_move(self):\n", + " # set the abort_event flag\n", + " self.abort_event.set()\n", + " # check to make sure both the monitor and simticks thread have terminated\n", + " if not self.monitor.is_alive() and not self.simticks.is_alive():\n", + " # now we can reset the abort_event flag\n", + " self.abort_event.clear()\n", + " return\n" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "new_state = format(led_status, '#020b')" + "fdome = FakeDome()" ] }, { "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0'" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list(new_state)[2]" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "class LED_lights(Flag):\n", - " POWER = 0b100000000000000000\n", - " COMMS = 0b010000000000000000\n", - " WARN = 0b001000000000000000\n", - " INPUT_1 = 0b000100000000000000\n", - " INPUT_2 = 0b000010000000000000\n", - " INPUT_3 = 0b000001000000000000\n", - " RELAY_3_NC = 0b000000100000000000\n", - " RELAY_3_NO = 0b000000010000000000\n", - " RELAY_2_NC = 0b000000001000000000\n", - " RELAY_2_NO = 0b000000000100000000\n", - " RELAY_1_NC = 0b000000000010000000\n", - " RELAY_1_NO = 0b000000000001000000\n", - " OUTPUT_3 = 0b000000000000100000\n", - " OUTPUT_2 = 0b000000000000010000\n", - " OUTPUT_1 = 0b000000000000001000\n", - " ADC_3 = 0b000000000000000100\n", - " ADC_2 = 0b000000000000000010\n", - " ADC_1 = 0b000000000000000001" - ] - }, - { - "cell_type": "code", - "execution_count": 45, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0b000000001010000000\n", - "0b000000001000000000\n", - "0b000000001000000000\n" + "encoder count recorded, 10 ticks remaining.\n", + "ENCODER COUNT INCREMENT, current count: 0\n", + "ENCODER COUNT INCREMENT, current count: 1\n", + "encoder count recorded, 8 ticks remaining.\n", + "ENCODER COUNT INCREMENT, current count: 2\n", + "encoder count recorded, 7 ticks remaining.\n", + "ENCODER COUNT INCREMENT, current count: 3\n", + "encoder count recorded, 6 ticks remaining.\n", + "ENCODER COUNT INCREMENT, current count: 4\n", + "encoder count recorded, 5 ticks remaining.\n" ] } ], "source": [ - "ledstatus = LED_lights.relay_2_normally_closed | LED_lights.relay_1_normally_closed\n", - "print(format(ledstatus.value, '#020b'))\n", - "ledstatus&=(~LED_lights.relay_1_normally_closed)\n", - "print(format(ledstatus.value, '#020b'))\n", - "ledstatus&=(~LED_lights.relay_1_normally_closed)\n", - "print(format(ledstatus.value, '#020b'))" + "fdome.move()" ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 6, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'0b000000001010000000'" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "ENCODER COUNT INCREMENT, current count: 5\n", + "encoder count recorded, 4 ticks remaining.\n" + ] } ], "source": [ - "format(ledstatus.value, '#020b')" + "fdome.abort_move()" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 6, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0b000000001000000000'" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "format(ledstatus2.value, '#020b')" + "fdome._encoder_count = 0" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'0b000000001000000000'" + "0" ] }, - "execution_count": 43, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "format(ledstatus3.value, '#020b')" + "fdome.encoder_count" ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ - "dt = 1" + "fdome.move()" ] }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "a\n" - ] - } - ], - "source": [ - "if dt is 1:\n", - " print('a')" - ] + "outputs": [], + "source": [] }, { "cell_type": "code", From 9186fe62b0a2db54acbead0e928fc4da763e9b1a Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 11 Oct 2019 20:07:25 +1100 Subject: [PATCH 2/7] initial implementation of non-blocking moves --- domehunter/dome_control.py | 132 ++++++++++++++++++++-------- examples/Untitled.ipynb | 47 +++++++++- examples/dome_control_example.ipynb | 11 +-- 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index ac40123..4741561 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -3,6 +3,7 @@ import math import sys +import threading import time import warnings import logging @@ -157,7 +158,7 @@ def __init__(self, # pin factory to release all the pins Device.pin_factory.reset() # set a timeout length in seconds for wait_for_active() calls - WAIT_TIMEOUT = 1 + WAIT_TIMEOUT = 2*60 # in testing mode we need to create a seperate pin object so we can # simulate the activation of our fake DIDs and DODs @@ -183,12 +184,12 @@ def __init__(self, # if the requested tolerance is less than degrees_per_tick use # degrees_per_tick for the tolerance self.az_position_tolerance = max( - self.az_position_tolerance, self.degrees_per_tick) + self.az_position_tolerance, 1.5 * self.degrees_per_tick) self.home_az = Longitude(home_azimuth * u.deg) # need something to let us know when dome is calibrating so home sensor # activation doesnt zero encoder counts self.calibrating = False - + # NOTE: this led setup needs to be done before setting any callback # functions # turn on the relay LEDs if we are debugging @@ -214,6 +215,11 @@ def __init__(self, # create a instance variable to track the dome motor encoder ticks self._encoder_count = 0 self._dome_az = None # Unknown + # create a threading abort event, to use for aborting movement commands + self._abort_event = threading.Event() + # creating a threading move event, to indicate when a move thread + # is active + self._move_event = threading.Event() # bounce_time settings gives the time in seconds that the device will # ignore additional activation signals logger.info(f'Connecting encoder on pin {encoder_pin_number}') @@ -294,7 +300,7 @@ def degrees_per_tick(self): # Methods ############################################################################### - """These map directly onto the AbstractMethods created by RPC.""" + """These correspond to the AbstractMethods created by RPC.""" def abort(self): """ @@ -305,7 +311,12 @@ def abort(self): # one way might be cut power to the automationHAT so the motor relays # will receive no voltage even if the relay is in the open position? logger.warning(f'Aborting dome movement') - self._stop_moving() + # set the abort event thread flag + self._abort_event.set() + # wait for threads to abort + while self._move_event.is_set(): + pass + self._abort_event.clear() def goto_az(self, az): """ @@ -320,30 +331,23 @@ def goto_az(self, az): if self.dome_az is None: return - target_az = Longitude(az * u.deg) - logger.debug(f'Goto target azimuth: {target_az}') + self.target_az = Longitude(az * u.deg) + logger.debug(f'Goto target azimuth: {self.target_az}') + logger.info(f'Goto target azimuth: {self.target_az}') # calculate delta_az, wrapping at 180 to ensure we take shortest route - delta_az = (target_az - self.dome_az).wrap_at(180 * u.degree) + delta_az = (self.target_az - self.dome_az).wrap_at(180 * u.degree) logger.debug(f'Delta azimuth: {delta_az}') + logger.info(f'Delta azimuth: {delta_az}') if delta_az > 0: self._rotate_dome(Direction.CW) else: self._rotate_dome(Direction.CCW) # wait until encoder count matches desired delta az - while True: - delta_az = self.current_direction * (target_az - self.dome_az).wrap_at('180d') - if delta_az <= self.az_position_tolerance: - break - if self.testing: - # if testing simulate a tick for every cycle of while loop - self._simulate_ticks(num_ticks=1) - - time.sleep(0.1) - - logger.debug('Stopping dome movement') - self._stop_moving() + goingto_az = threading.Thread(target=self._thread_condition, + args=(self._goto_az_condition,)) + goingto_az.start() def calibrate_dome_encoder_counts(self, num_cal_rotations=2): """ @@ -357,36 +361,40 @@ def calibrate_dome_encoder_counts(self, num_cal_rotations=2): """ # rotate the dome until we hit home, to give reference point self.find_home() - + while self._move_event.is_set(): + # wait for find_home() to finish + time.sleep(0.1) # pause to let things settle/get a noticeable blink of debug_lights time.sleep(0.5) - rotation_count = 0 self.calibrating = True # now set dome to rotate num_cal_rotations times so we can determine # the number of ticks per revolution + self._move_event.set() + self._rotate_dome(Direction.CW) + rotation_count = 0 while rotation_count < num_cal_rotations: - self._rotate_dome(Direction.CW) if self.testing: # tell the fake home sensor that we have left home - self._home_sensor_pin.drive_low() - self._simulate_ticks(num_ticks=10) + homepinlow = threading.Timer(3, + self._home_sensor_pin.drive_low) + homepinlow.start() + self._home_sensor.wait_for_inactive(timeout=self.wait_timeout) - self._home_sensor.wait_for_active(timeout=self.wait_timeout) if self.testing: - # tell the fake home sensor that we have come back to home - time.sleep(0.1) - self._home_sensor_pin.drive_high() - time.sleep(0.1) + self._simulate_ticks(num_ticks=10) + homepinhigh = threading.Timer(3, + self._home_sensor_pin.drive_high) + homepinhigh.start() + self._home_sensor.wait_for_active(timeout=self.wait_timeout) # without this pause the wait_for_active wasn't waiting (???) time.sleep(0.1) rotation_count += 1 self._stop_moving() - # set the azimuth per encoder tick factor based on how many ticks we # counted over n rotations self._degrees_per_tick = Angle( @@ -401,14 +409,21 @@ def find_home(self): Move Dome to home position. """ + if self._move_event.is_set(): + logger.debug('Movement command in progress') + raise Exception("Movement command in progress") # pragma: no cover + # iniate the movement and set the _move_event flag + self._move_event.set() self._rotate_dome(Direction.CW) time.sleep(0.1) - self._home_sensor.wait_for_active(timeout=self.wait_timeout) + homing = threading.Thread(target=self._thread_condition, + args=(self._find_home_condition,)) + homing.start() if self.testing: # in testing mode need to "fake" the activation of the home pin - time.sleep(0.5) - self._home_sensor_pin.drive_high() - + home_pin_high = threading.Timer(0.5, + self._home_sensor_pin.drive_high) + home_pin_high.start() self._stop_moving() def sync(self, az): @@ -419,6 +434,52 @@ def sync(self, az): # Private Methods ############################################################################### + def _thread_condition(self, trigger_condition): + """Condition monitoring thread for movement commands. + + Will return when: + - trigger_condition is true + - abort event is triggered + - thread running time exceeds self.wait_timeout + + """ + goingtoaz = False + if trigger_condition.__name__ == '_goto_az_condition': + goingtoaz = True + start = time.time() + while True: + time.sleep(0.1) + wait_time = time.time() - start + if trigger_condition(): + break + elif self._abort_event.is_set(): + break + elif wait_time > self.wait_timeout: + break + elif goingtoaz and self.testing is True: + # if testing simulate a tick for every cycle of while loop + self._simulate_ticks(num_ticks=1) + logger.debug('Stopping dome movement') + self._stop_moving() + return + + def _goto_az_condition(self): + """Determines if current azimuth is within tolerance of target azimuth. + + Returns + ------- + bool + Returns true if self.dome_az is within tolerance of target azimuth. + + """ + delta_az = self.current_direction * (self.target_az - self.dome_az).wrap_at('180d') + return delta_az <= self.az_position_tolerance + + def _find_home_condition(self): + """Send True if the dome is at home.""" + # need a callable method for _thread_condition + return self._home_sensor.is_active + def _set_at_home(self): """ Update home status to at home and debug LEDs (if enabled). @@ -558,6 +619,7 @@ def _stop_moving(self): """ logger.debug(f'Turning off rotation relay') self._rotation_relay.off() + self._move_event.clear() # update the debug LEDs self._change_led_state(0, leds=[LED_Lights.RELAY_1_NO]) self._change_led_state(1, leds=[LED_Lights.RELAY_1_NC]) diff --git a/examples/Untitled.ipynb b/examples/Untitled.ipynb index 6c1be0c..b9b7610 100644 --- a/examples/Untitled.ipynb +++ b/examples/Untitled.ipynb @@ -185,10 +185,53 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "def testcond(a):\n", + " return a>1" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "testcond(-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'testcond'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "testcond.__name__" + ] }, { "cell_type": "code", diff --git a/examples/dome_control_example.ipynb b/examples/dome_control_example.ipynb index 0d618a8..7405f83 100644 --- a/examples/dome_control_example.ipynb +++ b/examples/dome_control_example.ipynb @@ -47,15 +47,6 @@ "print(f'Dome rotates {testdome.degrees_per_tick} degrees per encoder tick. Current encoder count is {testdome.encoder_count}.')" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(testdome.encoder_count)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -97,7 +88,7 @@ "outputs": [], "source": [ "# now we can tell it to go to an azimuth of 300 degrees. The dome will realise it is quicker to rotate anticlockwise\n", - "testdome.GotoAz(300)" + "testdome.goto_az(300)" ] }, { From 82afd4e5aba481e1c8a76040b0b23508fc07390f Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 15 Oct 2019 01:39:35 +1100 Subject: [PATCH 3/7] fixing up the calibration thread --- domehunter/dome_control.py | 95 ++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index 4741561..8c5dc24 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -189,7 +189,11 @@ def __init__(self, # need something to let us know when dome is calibrating so home sensor # activation doesnt zero encoder counts self.calibrating = False - + # instance variable to track rotations during calibration + self.rotation_count = 0 + # instance variable that is over written in calibrate routine + self.num_cal_rotations = 2 + # NOTE: this led setup needs to be done before setting any callback # functions # turn on the relay LEDs if we are debugging @@ -258,7 +262,6 @@ def __init__(self, else: self._set_not_home() - ############################################################################### # Properties ############################################################################### @@ -331,6 +334,9 @@ def goto_az(self, az): if self.dome_az is None: return + # need target_az for _goto_az_condition but unsure how to pass it + # through the thread as an argument for _goto_az_condition. + # creating a instance variable as a work around. self.target_az = Longitude(az * u.deg) logger.debug(f'Goto target azimuth: {self.target_az}') logger.info(f'Goto target azimuth: {self.target_az}') @@ -368,41 +374,16 @@ def calibrate_dome_encoder_counts(self, num_cal_rotations=2): time.sleep(0.5) self.calibrating = True + # reset rotation_count instance variable to zero + self.rotation_count = 0 + self.num_cal_rotations = num_cal_rotations # now set dome to rotate num_cal_rotations times so we can determine # the number of ticks per revolution self._move_event.set() self._rotate_dome(Direction.CW) - rotation_count = 0 - while rotation_count < num_cal_rotations: - if self.testing: - # tell the fake home sensor that we have left home - homepinlow = threading.Timer(3, - self._home_sensor_pin.drive_low) - homepinlow.start() - - self._home_sensor.wait_for_inactive(timeout=self.wait_timeout) - - if self.testing: - self._simulate_ticks(num_ticks=10) - homepinhigh = threading.Timer(3, - self._home_sensor_pin.drive_high) - homepinhigh.start() - - self._home_sensor.wait_for_active(timeout=self.wait_timeout) - # without this pause the wait_for_active wasn't waiting (???) - time.sleep(0.1) - - rotation_count += 1 - - self._stop_moving() - # set the azimuth per encoder tick factor based on how many ticks we - # counted over n rotations - self._degrees_per_tick = Angle( - 360 / (self.encoder_count / rotation_count) * u.deg) - # update the az_position_tolerance - self.az_position_tolerance = max( - self.az_position_tolerance, 2 * self.degrees_per_tick) - self.calibrating = False + calibrating = threading.Thread(target=self._thread_condition, + args=(self._calibration_condition,)) + calibrating.start() def find_home(self): """ @@ -424,7 +405,6 @@ def find_home(self): home_pin_high = threading.Timer(0.5, self._home_sensor_pin.drive_high) home_pin_high.start() - self._stop_moving() def sync(self, az): self._encoder_count = self._az_to_ticks(Longitude(az * u.deg)) @@ -461,6 +441,10 @@ def _thread_condition(self, trigger_condition): self._simulate_ticks(num_ticks=1) logger.debug('Stopping dome movement') self._stop_moving() + # reset various dome state variables/events + self._move_event.clear() + self.calibrating = False + self.rotation_count = 0 return def _goto_az_condition(self): @@ -480,6 +464,46 @@ def _find_home_condition(self): # need a callable method for _thread_condition return self._home_sensor.is_active + def _calibration_condition(self): + """Send True once dome completes desired number of rotations""" + last_rotation_count = self.rotation_count + while True: + if self.rotation_count >= self.num_cal_rotations: + # if we have completed the number of calibration rotations, + # stop dome rotation. + self._stop_moving() + break + # for testing, we want to simulate the home activation/inactivation + # as well as ticks in a rotation. We only want this once per + # rotation but need a continuous polling condition to allow for + # immediate aborts. + # so here we check for a difference in the last loops rot count + # and the current one, so that the clause is only triggered + # once after each triggering of the home switch. + new_rotation = self.rotation_count - last_rotation_count > 0 + if self.testing and new_rotation: + last_rotation_count = self.rotation_count + # tell fake home pin that we have left home + homepinlow = threading.Timer(0.5, + self._home_sensor_pin.drive_low) + # simulate 10 ticks after faking home sensor deactivation + simticks = threading.Timer(1.0, + self._simulate_ticks(num_ticks=10)) + homepinhigh = threading.Timer(5, + self._home_sensor_pin.drive_high) + homepinlow.start() + simticks.start() + homepinhigh.start() + time.sleep(0.1) + # set the azimuth per encoder tick factor based on how many ticks we + # counted over n rotations + self._degrees_per_tick = Angle( + 360 / (self.encoder_count / self.rotation_count) * u.deg) + # update the az_position_tolerance + self.az_position_tolerance = max( + self.az_position_tolerance, 1.5 * self.degrees_per_tick) + return + def _set_at_home(self): """ Update home status to at home and debug LEDs (if enabled). @@ -491,6 +515,9 @@ def _set_at_home(self): # add 1 to self.current_direction, to get CCW to evaluate to False if not self.calibrating and bool(self.current_direction + 1): self._encoder_count = 0 + # if we are calibrating, increment the rotation count + if self.calibrating: + self.rotation_count += 1 def _set_not_home(self): """ From f22dad1614e8ea1d304db6f7aa4da4d3feba5975 Mon Sep 17 00:00:00 2001 From: fergus Date: Wed, 16 Oct 2019 19:24:16 +1100 Subject: [PATCH 4/7] non-blocking movement commands working/passing tests --- domehunter/dome_control.py | 137 +++++++++----- domehunter/tests/test_domehunter.py | 48 ++--- examples/Untitled.ipynb | 265 ---------------------------- examples/dome_control_example.ipynb | 90 +++++++--- 4 files changed, 189 insertions(+), 351 deletions(-) delete mode 100644 examples/Untitled.ipynb diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index 8c5dc24..84b241a 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -180,11 +180,7 @@ def __init__(self, # TODO: read in default value from yaml(?) self._degrees_per_tick = Angle(degrees_per_tick * u.deg) - self.az_position_tolerance = Angle(az_position_tolerance * u.deg) - # if the requested tolerance is less than degrees_per_tick use - # degrees_per_tick for the tolerance - self.az_position_tolerance = max( - self.az_position_tolerance, 1.5 * self.degrees_per_tick) + self._az_position_tolerance = Angle(az_position_tolerance * u.deg) self.home_az = Longitude(home_azimuth * u.deg) # need something to let us know when dome is calibrating so home sensor # activation doesnt zero encoder counts @@ -224,6 +220,9 @@ def __init__(self, # creating a threading move event, to indicate when a move thread # is active self._move_event = threading.Event() + # creating a threading simulated_rotation event, to indicate when a + # simulated rotation thread is running for testing mode calibration + self._simulated_rotation_event = threading.Event() # bounce_time settings gives the time in seconds that the device will # ignore additional activation signals logger.info(f'Connecting encoder on pin {encoder_pin_number}') @@ -248,6 +247,8 @@ def __init__(self, # because we initialiase the relay in the normally closed position logger.info(f'Setting start direction to CCW') self.current_direction = Direction.CCW + # need to set something for last direction now + self.last_direction = Direction.NONE # Home Sensor logger.info(f'Connecting home sensor on pin {home_sensor_pin_number}') @@ -268,7 +269,7 @@ def __init__(self, @property def dome_az(self): - """ """ + """Returns the dome azimuth in degrees.""" if self._dome_az is None: print("Cannot return Azimuth as Dome is not yet calibrated. Run calibration loop") logger.debug(f'Dome azimuth: {self._dome_az}') @@ -276,7 +277,7 @@ def dome_az(self): @property def at_home(self): - """Send True if the dome is at home.""" + """Return True if the dome is at home.""" home_active = self._home_sensor.is_active logger.debug(f'Home active: {home_active}') return home_active @@ -288,6 +289,11 @@ def dome_in_motion(self): logger.debug(f'Dome in motion: {dome_motion}') return dome_motion + @property + def movement_thread_active(self): + """Return true if a movement thread is running""" + return self._move_event.is_set() + @property def encoder_count(self): """Returns the current encoder count.""" @@ -299,6 +305,20 @@ def degrees_per_tick(self): """Returns the calibrated azimuth (in degrees) per encoder tick.""" logger.debug(f'Degrees per tick: {self._degrees_per_tick}') return self._degrees_per_tick + + @property + def az_position_tolerance(self): + """ + Returns the azimuth position tolerance (in degrees). + + If the tolerance set at initialisation is less than degrees_per_tick + use 1.5 * degrees_per_tick as the tolerance. + """ + # if the requested tolerance is less than degrees_per_tick use + # degrees_per_tick for the tolerance + tolerance = max(self._az_position_tolerance, + 1.5 * self.degrees_per_tick) + return tolerance ############################################################################### # Methods ############################################################################### @@ -333,19 +353,20 @@ def goto_az(self, az): """ if self.dome_az is None: return - + if self._move_event.is_set(): + logger.debug('Movement command in progress') + raise Exception("Movement command in progress") # need target_az for _goto_az_condition but unsure how to pass it # through the thread as an argument for _goto_az_condition. # creating a instance variable as a work around. self.target_az = Longitude(az * u.deg) - logger.debug(f'Goto target azimuth: {self.target_az}') - logger.info(f'Goto target azimuth: {self.target_az}') + logger.debug(f'goto_az: Goto target azimuth [{self.target_az}]') # calculate delta_az, wrapping at 180 to ensure we take shortest route delta_az = (self.target_az - self.dome_az).wrap_at(180 * u.degree) - logger.debug(f'Delta azimuth: {delta_az}') - logger.info(f'Delta azimuth: {delta_az}') + logger.debug(f'goto_az: Delta azimuth [{delta_az}]') + self._move_event.set() if delta_az > 0: self._rotate_dome(Direction.CW) else: @@ -365,25 +386,31 @@ def calibrate_dome_encoder_counts(self, num_cal_rotations=2): Number of rotations to perform to calibrate encoder. """ + if self._move_event.is_set(): + logger.debug('Movement command in progress') + raise Exception("Movement command in progress") # rotate the dome until we hit home, to give reference point + logger.debug(f'calibrate_dome_encoder_counts: Finding Home') self.find_home() while self._move_event.is_set(): # wait for find_home() to finish time.sleep(0.1) + logger.debug(f'calibrate_dome_encoder_counts: Found Home') # pause to let things settle/get a noticeable blink of debug_lights time.sleep(0.5) - self.calibrating = True # reset rotation_count instance variable to zero self.rotation_count = 0 self.num_cal_rotations = num_cal_rotations # now set dome to rotate num_cal_rotations times so we can determine # the number of ticks per revolution + logger.debug(f'calibrate_dome_encoder_counts: Starting calibration rotations.') self._move_event.set() self._rotate_dome(Direction.CW) - calibrating = threading.Thread(target=self._thread_condition, + self.calibrating = True + calibrate = threading.Thread(target=self._thread_condition, args=(self._calibration_condition,)) - calibrating.start() + calibrate.start() def find_home(self): """ @@ -392,8 +419,9 @@ def find_home(self): """ if self._move_event.is_set(): logger.debug('Movement command in progress') - raise Exception("Movement command in progress") # pragma: no cover + raise Exception("Movement command in progress") # iniate the movement and set the _move_event flag + logger.debug(f'find_home: Finding Home') self._move_event.set() self._rotate_dome(Direction.CW) time.sleep(0.1) @@ -407,6 +435,15 @@ def find_home(self): home_pin_high.start() def sync(self, az): + """ + Sync encoder count to azimuth position. + Parameters + ---------- + az : float + Desired dome azimuth position in degrees. + + """ + logger.debug(f'sync: syncing encoder counts to azimuth [{az}]') self._encoder_count = self._az_to_ticks(Longitude(az * u.deg)) return @@ -431,15 +468,18 @@ def _thread_condition(self, trigger_condition): time.sleep(0.1) wait_time = time.time() - start if trigger_condition(): + logger.debug(f'Monitoring thread triggered by {trigger_condition.__name__}') break elif self._abort_event.is_set(): + logger.debug(f'Monitoring thread triggered by abort event') break elif wait_time > self.wait_timeout: + logger.debug(f'Monitoring thread triggered by timeout [{self.wait_timeout}s]') break elif goingtoaz and self.testing is True: # if testing simulate a tick for every cycle of while loop self._simulate_ticks(num_ticks=1) - logger.debug('Stopping dome movement') + logger.debug('Monitor Thread: Stopping dome movement') self._stop_moving() # reset various dome state variables/events self._move_event.clear() @@ -457,6 +497,7 @@ def _goto_az_condition(self): """ delta_az = self.current_direction * (self.target_az - self.dome_az).wrap_at('180d') + logger.debug(f'Delta_az is {delta_az}') return delta_az <= self.az_position_tolerance def _find_home_condition(self): @@ -466,13 +507,16 @@ def _find_home_condition(self): def _calibration_condition(self): """Send True once dome completes desired number of rotations""" - last_rotation_count = self.rotation_count + start = time.time() + # if self.testing: + # for i in range(self.num_cal_rotations): + # pass + first_rotation = threading.Thread(target=self._simulate_rotation) + first_rotation.start() + while True: - if self.rotation_count >= self.num_cal_rotations: - # if we have completed the number of calibration rotations, - # stop dome rotation. - self._stop_moving() - break + logger.debug(f'Loop {time.time() - start}: rot_count [{self.rotation_count}], encoder_count [{self.encoder_count}]') + # for testing, we want to simulate the home activation/inactivation # as well as ticks in a rotation. We only want this once per # rotation but need a continuous polling condition to allow for @@ -480,43 +524,40 @@ def _calibration_condition(self): # so here we check for a difference in the last loops rot count # and the current one, so that the clause is only triggered # once after each triggering of the home switch. - new_rotation = self.rotation_count - last_rotation_count > 0 - if self.testing and new_rotation: - last_rotation_count = self.rotation_count - # tell fake home pin that we have left home - homepinlow = threading.Timer(0.5, - self._home_sensor_pin.drive_low) - # simulate 10 ticks after faking home sensor deactivation - simticks = threading.Timer(1.0, - self._simulate_ticks(num_ticks=10)) - homepinhigh = threading.Timer(5, - self._home_sensor_pin.drive_high) - homepinlow.start() - simticks.start() - homepinhigh.start() - time.sleep(0.1) + logger.debug(f'Simulated rotation underway {self._simulated_rotation_event.is_set()}') + if self.testing and not self._simulated_rotation_event.is_set(): + logger.debug(f'Calibration monitor thread: Completion of simulated rotation detected, simulating next rotation.') + + simulated_rotation = threading.Thread(target=self._simulate_rotation) + if self.rotation_count >= self.num_cal_rotations: + # if we have completed the number of calibration rotations, + # stop dome rotation. + logger.debug(f'Calibration monitor thread: calibration rotations completed, ending calibration.') + self._stop_moving() + break + simulated_rotation.start() + time.sleep(1) # set the azimuth per encoder tick factor based on how many ticks we # counted over n rotations self._degrees_per_tick = Angle( 360 / (self.encoder_count / self.rotation_count) * u.deg) - # update the az_position_tolerance - self.az_position_tolerance = max( - self.az_position_tolerance, 1.5 * self.degrees_per_tick) - return + return True def _set_at_home(self): """ Update home status to at home and debug LEDs (if enabled). """ - logger.debug('Stopping dome movement') + logger.info('Home sensor activated') self._change_led_state(1, leds=[LED_Lights.INPUT_2]) # don't want to zero encoder while calibrating # note: because Direction.CW is +1 and Direction.CCW is -1, need to # add 1 to self.current_direction, to get CCW to evaluate to False if not self.calibrating and bool(self.current_direction + 1): + logger.debug(f'_set_at_home: Passing home clockwise, zeroing encoder counts.') self._encoder_count = 0 # if we are calibrating, increment the rotation count if self.calibrating: + logger.debug(f'_set_at_home: Home triggered during calibration, incrementing calibration rotation count.') self.rotation_count += 1 def _set_not_home(self): @@ -671,6 +712,16 @@ def _simulate_ticks(self, num_ticks): self._encoder_pin.drive_high() time.sleep(self.test_mode_delay_duration) + def _simulate_rotation(self, ticks_per_rotation=10): + """ + Method to simulate a complete dome rotation while in testing mode. + """ + self._simulated_rotation_event.set() + self._home_sensor_pin.drive_low() + self._simulate_ticks(ticks_per_rotation) + self._home_sensor_pin.drive_high() + self._simulated_rotation_event.clear() + def _change_led_state(self, desired_state, leds=[]): # pragma: no cover """ Method of turning a set of debugging LEDs on diff --git a/domehunter/tests/test_domehunter.py b/domehunter/tests/test_domehunter.py index 3a26053..efa74d2 100644 --- a/domehunter/tests/test_domehunter.py +++ b/domehunter/tests/test_domehunter.py @@ -1,5 +1,5 @@ import pytest - +import time from domehunter.dome_control import * @@ -22,7 +22,7 @@ def dome_az_90(scope='function'): def test_dome_initialisation(testing_dome): assert testing_dome.testing is True assert testing_dome.dome_in_motion is False - assert testing_dome.dome_az.degree == 0 + assert testing_dome.dome_az is None assert testing_dome.encoder_count == 0 assert testing_dome.degrees_per_tick == Angle(1 * u.deg) assert testing_dome.at_home is False @@ -43,34 +43,42 @@ def test_status(testing_dome): assert testing_dome.dome_in_motion is False -def test_abort(testing_dome): - # This test should test that GotoAz() is non-blocking - # but haven't implemented that yet - testing_dome._rotation_relay.on() - assert testing_dome.dome_in_motion is True - testing_dome.abort() - assert testing_dome.dome_in_motion is False +def test_abort(dome_az_90): + dome_az_90.goto_az(300) + time.sleep(0.5) + assert dome_az_90.movement_thread_active + assert dome_az_90.dome_in_motion + time.sleep(0.5) + dome_az_90.abort() + assert not dome_az_90.movement_thread_active + assert not dome_az_90.dome_in_motion -def test_dome_az(dome_az_90): - assert dome_az_90.dome_az == 90 - assert dome_az_90.dome_az == dome_az_90.dome_az.degree +def test_dome_az(dome_az_90, testing_dome): + assert dome_az_90.dome_az == Longitude(90 * u.deg) + assert testing_dome.dome_az is None -def test_GotoAz(dome_az_90): +def test_goto_az(dome_az_90): # test fixture has degrees_per_tick attribute of 10 - dome_az_90.GotoAz(300) - assert dome_az_90.dome_az == Angle(300 * u.deg) - assert dome_az_90.encoder_count == -6 - dome_az_90.GotoAz(2) - assert dome_az_90.dome_az == Angle(10 * u.deg) - assert dome_az_90.encoder_count == 1 + dome_az_90.goto_az(300) + while dome_az_90.movement_thread_active: + time.sleep(1) + assert dome_az_90.dome_az == Angle(310 * u.deg) + assert dome_az_90.encoder_count == -5 + dome_az_90.goto_az(2) + while dome_az_90.movement_thread_active: + time.sleep(1) + assert dome_az_90.dome_az == Angle(350 * u.deg) + assert dome_az_90.encoder_count == -1 @pytest.mark.calibrate def test_calibrate_dome_encoder_counts(testing_dome): testing_dome.calibrate_dome_encoder_counts() - assert testing_dome.dome_az == 0 + assert testing_dome.dome_az is None + while testing_dome.movement_thread_active: + time.sleep(1) assert testing_dome.encoder_count == 20 assert testing_dome.degrees_per_tick == Angle(36 * u.deg) diff --git a/examples/Untitled.ipynb b/examples/Untitled.ipynb deleted file mode 100644 index b9b7610..0000000 --- a/examples/Untitled.ipynb +++ /dev/null @@ -1,265 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import threading\n", - "import time" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Thread, Timer, Event and Lock. Those are the bits you'll need.\n", - "\n", - "# For the non-blocking moves the basic idea is that you write a function that contains the \n", - "# while loop that checks when it's time to stop the move, you create a Thread and pass it \n", - "# that function, start the move, start the Thread, then return. The Thread will run in the \n", - "# background and stop the move then at target. (edited)\n", - "\n", - "# You can use Events to do simple communication between threads, e.g. you can pass one to \n", - "# the dome movement thread and use it to stop the move early if the abort function is called." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import threading\n", - "import time\n", - "\n", - "class FakeDome(object):\n", - " def __init__(self):\n", - " self._encoder_count = 0\n", - " self.abort_event = threading.Event()\n", - "\n", - " @property\n", - " def encoder_count(self):\n", - " return self._encoder_count\n", - "\n", - " def move(self, ticks=10):\n", - " # dummy move command that just moves us 10 ticks from current encoder position\n", - " target_count = self.encoder_count + ticks\n", - " # create the monitor thread that tracks completion of movement command\n", - " self.monitor = threading.Thread(target=self.move_condition, args=[target_count])\n", - " # create a thread to simulate ticks\n", - " self.simticks = threading.Thread(target=self.simulate_ticks, args=[ticks])\n", - " # start both threads\n", - " self.monitor.start()\n", - " self.simticks.start()\n", - " return\n", - " \n", - " def move_condition(self, target_count):\n", - " # record the start time, to check for timeout condition\n", - " start = time.time()\n", - " while self.encoder_count < target_count:\n", - " # check to see if abort_event is set and end move() if it is\n", - " if self.abort_event.is_set():\n", - " return\n", - " # short break to space ticks/status-checks out\n", - " time.sleep(1)\n", - " print(f'encoder count recorded, {target_count - self.encoder_count} ticks remaining.')\n", - " # if monitor thread has been going for more than 15s end it\n", - " if time.time() - start > 15:\n", - " return\n", - " return\n", - " \n", - " def simulate_ticks(self, ticks):\n", - " for tick in range(ticks):\n", - " # check to see if abort_event is set and end move() if it is\n", - " if self.abort_event.is_set():\n", - " return\n", - " time.sleep(1)\n", - " print(f'ENCODER COUNT INCREMENT, current count: {self.encoder_count}')\n", - " self._encoder_count+=1\n", - " return\n", - " \n", - " def abort_move(self):\n", - " # set the abort_event flag\n", - " self.abort_event.set()\n", - " # check to make sure both the monitor and simticks thread have terminated\n", - " if not self.monitor.is_alive() and not self.simticks.is_alive():\n", - " # now we can reset the abort_event flag\n", - " self.abort_event.clear()\n", - " return\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "fdome = FakeDome()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "encoder count recorded, 10 ticks remaining.\n", - "ENCODER COUNT INCREMENT, current count: 0\n", - "ENCODER COUNT INCREMENT, current count: 1\n", - "encoder count recorded, 8 ticks remaining.\n", - "ENCODER COUNT INCREMENT, current count: 2\n", - "encoder count recorded, 7 ticks remaining.\n", - "ENCODER COUNT INCREMENT, current count: 3\n", - "encoder count recorded, 6 ticks remaining.\n", - "ENCODER COUNT INCREMENT, current count: 4\n", - "encoder count recorded, 5 ticks remaining.\n" - ] - } - ], - "source": [ - "fdome.move()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ENCODER COUNT INCREMENT, current count: 5\n", - "encoder count recorded, 4 ticks remaining.\n" - ] - } - ], - "source": [ - "fdome.abort_move()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "fdome._encoder_count = 0" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "fdome.encoder_count" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "fdome.move()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "def testcond(a):\n", - " return a>1" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "testcond(-1)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'testcond'" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "testcond.__name__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/dome_control_example.ipynb b/examples/dome_control_example.ipynb index 7405f83..e8b1f3b 100644 --- a/examples/dome_control_example.ipynb +++ b/examples/dome_control_example.ipynb @@ -9,8 +9,25 @@ "from domehunter.dome_control import Dome\n", "import astropy.units as u\n", "from astropy.coordinates import Angle, Longitude\n", - "# When the domehunter package tries to import the sn3218 library it will either find it isn't installed, or it wont detect the hardware it is expecting\n", - "# in both cases a warning will be raised. If you are testing without the automationHAT this warning can be ignored." + "\n", + "\"\"\"\n", + "When the domehunter package tries to import the sn3218 library it will either find it isn't installed, \n", + "or it wont detect the hardware it is expecting in both cases a warning will be raised. If you are \n", + "testing without the automationHAT this warning can be ignored.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# set the loggin level\n", + "import logging\n", + "logger = logging.getLogger()\n", + "logger.setLevel(logging.INFO)\n", + "#logger.setLevel(logging.DEBUG)" ] }, { @@ -19,10 +36,15 @@ "metadata": {}, "outputs": [], "source": [ - "# testing=True means all the automationHAT functionality and the state of the GPIOzero pins will be mocked/simulated\n", - "# debug_lights=True means the automationHAT status LEDs will be enabled on the automationHAT. If you do not have an automationHAT this should be set to False.\n", + "\"\"\"\n", + "testing=True means all the automationHAT functionality and the state of the GPIOzero pins will be \n", + "mocked/simulated debug_lights=True means the automationHAT status LEDs will be enabled on the \n", + "automationHAT. If you do not have an automationHAT this should be set to False.\n", + "\n", + "NB at the moment if you try and create Dome twice it wont work because the gpio pins from the \n", + "first instance wont be released.\n", + "\"\"\"\n", "\n", - "# NB at the moment if you try and create Dome twice it wont work because the gpio pins from the first instance wont be released.\n", "testdome = Dome(testing=True, debug_lights=False)" ] }, @@ -32,8 +54,12 @@ "metadata": {}, "outputs": [], "source": [ - "# the calibrate method tells the dome to rotate n times (default n=2) and use the encoder counts to determine the degrees of rotation per encoder tick\n", - "# In testing mode, we will simulate 10 ticks per rotation, for 20 ticks total.\n", + "\"\"\"\n", + "The calibrate method tells the dome to rotate n times (default n=2) and use the encoder counts to \n", + "determine the degrees of rotation per encoder tick.\n", + "In testing mode, we will simulate 10 ticks per rotation, for 20 ticks total.\n", + "\"\"\"\n", + "\n", "testdome.calibrate_dome_encoder_counts()" ] }, @@ -43,7 +69,10 @@ "metadata": {}, "outputs": [], "source": [ - "# We can now check the the degrees per tick factor and the encoder count\n", + "\"\"\"\n", + "We can now check the the degrees per tick factor and the encoder count\n", + "\"\"\"\n", + "\n", "print(f'Dome rotates {testdome.degrees_per_tick} degrees per encoder tick. Current encoder count is {testdome.encoder_count}.')" ] }, @@ -53,8 +82,12 @@ "metadata": {}, "outputs": [], "source": [ - "# If we are in testing mode, lets now tell it that is at an azimuth of 90 degrees, an encoder count of 9 and that it rotates 10 degrees per tick.\n", - "# ND these are all private member variables, so to access them we need to use the \"name mangled\" member variable names\n", + "\"\"\"\n", + "If we are in testing mode, lets now tell it that is at an azimuth of 90 degrees, an encoder count \n", + "of 9 and that it rotates 10 degrees per tick. ND these are all private member variables, so to \n", + "access them we need to use the \"name mangled\" member variable names\n", + "\"\"\"\n", + "\n", "testdome._encoder_count = 9\n", "testdome._degrees_per_tick = Angle(10 * u.deg)\n", "testdome._dome_az = Angle(90 * u.deg)" @@ -68,7 +101,8 @@ "source": [ "print(testdome.encoder_count)\n", "print(testdome.degrees_per_tick)\n", - "print(testdome.dome_az)" + "print(testdome.dome_az)\n", + "print(testdome.az_position_tolerance)" ] }, { @@ -77,7 +111,8 @@ "metadata": {}, "outputs": [], "source": [ - "# check where the dome thinks it is\n", + "\"\"\"Check where the dome thinks it is\"\"\"\n", + "\n", "testdome.dome_az" ] }, @@ -87,7 +122,8 @@ "metadata": {}, "outputs": [], "source": [ - "# now we can tell it to go to an azimuth of 300 degrees. The dome will realise it is quicker to rotate anticlockwise\n", + "\"\"\"Now we can tell it to go to an azimuth of 300 degrees. The dome will realise it is quicker to rotate anticlockwise\"\"\"\n", + "\n", "testdome.goto_az(300)" ] }, @@ -99,7 +135,8 @@ "source": [ "print(testdome.encoder_count)\n", "print(testdome.degrees_per_tick)\n", - "print(testdome.dome_az)" + "print(testdome.dome_az)\n", + "print(testdome.az_position_tolerance)" ] }, { @@ -108,7 +145,8 @@ "metadata": {}, "outputs": [], "source": [ - "# we can now check if the dome ended up where we wanted.\n", + "\"\"\"We can now check if the dome ended up where we wanted.\"\"\"\n", + "\n", "print(f'Dome is currently at an azimuth of {testdome.dome_az}, with an encoder count of {testdome.encoder_count}')" ] }, @@ -118,10 +156,14 @@ "metadata": {}, "outputs": [], "source": [ - "# currently the dome will overshoot the position depending on how fine the az_per_tick instance variable is (10 degrees is pretty coarse).\n", - "# The dome azimuth is only updated according to how many ticks were recorded, so even if it overshoots it should still know where it is.\n", - "# after every movement, once the dome_az is update the encoder is set to the corresponding number of ticks as if it had just rotated from\n", - "# azimuth of zero to the current location (encoder_count = dome_az/az_per_tick)" + "\"\"\"\n", + "Currently the dome will overshoot the position depending on how fine the az_per_tick instance \n", + "variable is (10 degrees is pretty coarse). The dome azimuth is only updated according to how \n", + "many ticks were recorded, so even if it overshoots it should still know where it is. After \n", + "every movement, once the dome_az is update the encoder is set to the corresponding number of \n", + "ticks as if it had just rotated from azimuth of zero to the current location \n", + "(encoder_count = dome_az/az_per_tick)\n", + "\"\"\"" ] }, { @@ -130,8 +172,9 @@ "metadata": {}, "outputs": [], "source": [ - "# now send the dome to an azimuth of 2 degrees, in this case the dome will decide to rotate clockwise.\n", - "testdome.GotoAz(2)" + "\"\"\"Now send the dome to an azimuth of 2 degrees, in this case the dome will decide to rotate clockwise.\"\"\"\n", + "\n", + "testdome.goto_az(2)" ] }, { @@ -140,7 +183,8 @@ "metadata": {}, "outputs": [], "source": [ - "# we can now check if the dome ended up where we wanted.\n", + "\"\"\"We can now check if the dome ended up where we wanted.\"\"\"\n", + "\n", "print(f'Dome is currently at an azimuth of {testdome.dome_az}, with an encoder count of {testdome.encoder_count}')" ] }, @@ -168,7 +212,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.6.7" } }, "nbformat": 4, From 46ba49443e57d554a6757c6bda82b4098468e9d5 Mon Sep 17 00:00:00 2001 From: fergus Date: Wed, 16 Oct 2019 20:01:49 +1100 Subject: [PATCH 5/7] fixing broken calibration thread condition --- domehunter/dome_control.py | 37 +++++++++++++---------------- examples/dome_control_example.ipynb | 32 ++++++++++++------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index 84b241a..4fa7030 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -184,7 +184,7 @@ def __init__(self, self.home_az = Longitude(home_azimuth * u.deg) # need something to let us know when dome is calibrating so home sensor # activation doesnt zero encoder counts - self.calibrating = False + self._calibrating = False # instance variable to track rotations during calibration self.rotation_count = 0 # instance variable that is over written in calibrate routine @@ -407,10 +407,12 @@ def calibrate_dome_encoder_counts(self, num_cal_rotations=2): logger.debug(f'calibrate_dome_encoder_counts: Starting calibration rotations.') self._move_event.set() self._rotate_dome(Direction.CW) - self.calibrating = True - calibrate = threading.Thread(target=self._thread_condition, - args=(self._calibration_condition,)) + self._calibrating = True + calibrate = threading.Thread(target=self._calibrate) + cal_monitor = threading.Thread(target=self._thread_condition, + args=(self._calibration_completed,)) calibrate.start() + cal_monitor.start() def find_home(self): """ @@ -483,7 +485,7 @@ def _thread_condition(self, trigger_condition): self._stop_moving() # reset various dome state variables/events self._move_event.clear() - self.calibrating = False + self._calibrating = False self.rotation_count = 0 return @@ -501,12 +503,17 @@ def _goto_az_condition(self): return delta_az <= self.az_position_tolerance def _find_home_condition(self): - """Send True if the dome is at home.""" + """Return True if the dome is at home.""" # need a callable method for _thread_condition return self._home_sensor.is_active - def _calibration_condition(self): - """Send True once dome completes desired number of rotations""" + def _calibration_completed(self): + """Return True if both calibrating and move_event flags are True""" + cal_in_progress = self._calibrating and self._move_event.is_set() + return not cal_in_progress + + def _calibrate(self): + """Calibration method to be called from within a non-blocking thread""" start = time.time() # if self.testing: # for i in range(self.num_cal_rotations): @@ -516,18 +523,9 @@ def _calibration_condition(self): while True: logger.debug(f'Loop {time.time() - start}: rot_count [{self.rotation_count}], encoder_count [{self.encoder_count}]') - - # for testing, we want to simulate the home activation/inactivation - # as well as ticks in a rotation. We only want this once per - # rotation but need a continuous polling condition to allow for - # immediate aborts. - # so here we check for a difference in the last loops rot count - # and the current one, so that the clause is only triggered - # once after each triggering of the home switch. logger.debug(f'Simulated rotation underway {self._simulated_rotation_event.is_set()}') if self.testing and not self._simulated_rotation_event.is_set(): logger.debug(f'Calibration monitor thread: Completion of simulated rotation detected, simulating next rotation.') - simulated_rotation = threading.Thread(target=self._simulate_rotation) if self.rotation_count >= self.num_cal_rotations: # if we have completed the number of calibration rotations, @@ -541,7 +539,6 @@ def _calibration_condition(self): # counted over n rotations self._degrees_per_tick = Angle( 360 / (self.encoder_count / self.rotation_count) * u.deg) - return True def _set_at_home(self): """ @@ -552,11 +549,11 @@ def _set_at_home(self): # don't want to zero encoder while calibrating # note: because Direction.CW is +1 and Direction.CCW is -1, need to # add 1 to self.current_direction, to get CCW to evaluate to False - if not self.calibrating and bool(self.current_direction + 1): + if not self._calibrating and bool(self.current_direction + 1): logger.debug(f'_set_at_home: Passing home clockwise, zeroing encoder counts.') self._encoder_count = 0 # if we are calibrating, increment the rotation count - if self.calibrating: + if self._calibrating: logger.debug(f'_set_at_home: Home triggered during calibration, incrementing calibration rotation count.') self.rotation_count += 1 diff --git a/examples/dome_control_example.ipynb b/examples/dome_control_example.ipynb index e8b1f3b..d79df06 100644 --- a/examples/dome_control_example.ipynb +++ b/examples/dome_control_example.ipynb @@ -6,15 +6,16 @@ "metadata": {}, "outputs": [], "source": [ - "from domehunter.dome_control import Dome\n", - "import astropy.units as u\n", - "from astropy.coordinates import Angle, Longitude\n", - "\n", "\"\"\"\n", "When the domehunter package tries to import the sn3218 library it will either find it isn't installed, \n", "or it wont detect the hardware it is expecting in both cases a warning will be raised. If you are \n", "testing without the automationHAT this warning can be ignored.\n", - "\"\"\"" + "\"\"\"\n", + "\n", + "from domehunter.dome_control import Dome\n", + "import astropy.units as u\n", + "from astropy.coordinates import Angle, Longitude\n", + "import time" ] }, { @@ -124,7 +125,9 @@ "source": [ "\"\"\"Now we can tell it to go to an azimuth of 300 degrees. The dome will realise it is quicker to rotate anticlockwise\"\"\"\n", "\n", - "testdome.goto_az(300)" + "testdome.goto_az(300)\n", + "while testdome.movement_thread_active:\n", + " time.sleep(1)" ] }, { @@ -163,18 +166,13 @@ "every movement, once the dome_az is update the encoder is set to the corresponding number of \n", "ticks as if it had just rotated from azimuth of zero to the current location \n", "(encoder_count = dome_az/az_per_tick)\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"Now send the dome to an azimuth of 2 degrees, in this case the dome will decide to rotate clockwise.\"\"\"\n", "\n", - "testdome.goto_az(2)" + "Now send the dome to an azimuth of 2 degrees, in this case the dome will decide to rotate clockwise.\n", + "\"\"\"\n", + "\n", + "testdome.goto_az(2)\n", + "while testdome.movement_thread_active:\n", + " time.sleep(1)" ] }, { From 62be24c7bcbb023bff83132db7f229d90583fd9c Mon Sep 17 00:00:00 2001 From: fergus Date: Thu, 17 Oct 2019 20:03:21 +1100 Subject: [PATCH 6/7] Anthony and Wilfred review/suggested changes --- domehunter/dome_control.py | 127 ++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index 4fa7030..a9ca51f 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -185,10 +185,6 @@ def __init__(self, # need something to let us know when dome is calibrating so home sensor # activation doesnt zero encoder counts self._calibrating = False - # instance variable to track rotations during calibration - self.rotation_count = 0 - # instance variable that is over written in calibrate routine - self.num_cal_rotations = 2 # NOTE: this led setup needs to be done before setting any callback # functions @@ -314,8 +310,8 @@ def az_position_tolerance(self): If the tolerance set at initialisation is less than degrees_per_tick use 1.5 * degrees_per_tick as the tolerance. """ - # if the requested tolerance is less than degrees_per_tick use - # degrees_per_tick for the tolerance + if self._az_position_tolerance < 1.5 * self.degrees_per_tick: + logger.warning(f'az_position_tolerance [{self._az_position_tolerance}] is less than 1.5 times degrees_per_tick. Setting tolerance to 1.5 * degrees_per_tick') tolerance = max(self._az_position_tolerance, 1.5 * self.degrees_per_tick) return tolerance @@ -337,7 +333,8 @@ def abort(self): # set the abort event thread flag self._abort_event.set() # wait for threads to abort - while self._move_event.is_set(): + while self.movement_thread_active: + time.sleep(0.1) pass self._abort_event.clear() @@ -353,17 +350,15 @@ def goto_az(self, az): """ if self.dome_az is None: return - if self._move_event.is_set(): - logger.debug('Movement command in progress') - raise Exception("Movement command in progress") - # need target_az for _goto_az_condition but unsure how to pass it - # through the thread as an argument for _goto_az_condition. - # creating a instance variable as a work around. - self.target_az = Longitude(az * u.deg) - logger.debug(f'goto_az: Goto target azimuth [{self.target_az}]') + if self.movement_thread_active: + logger.warning('Movement command in progress') + return + + target_az = Longitude(az * u.deg) + logger.debug(f'goto_az: Goto target azimuth [{target_az}]') # calculate delta_az, wrapping at 180 to ensure we take shortest route - delta_az = (self.target_az - self.dome_az).wrap_at(180 * u.degree) + delta_az = (target_az - self.dome_az).wrap_at(180 * u.degree) logger.debug(f'goto_az: Delta azimuth [{delta_az}]') self._move_event.set() @@ -373,7 +368,8 @@ def goto_az(self, az): self._rotate_dome(Direction.CCW) # wait until encoder count matches desired delta az goingto_az = threading.Thread(target=self._thread_condition, - args=(self._goto_az_condition,)) + args=(self._goto_az_complete, + target_az)) goingto_az.start() def calibrate_dome_encoder_counts(self, num_cal_rotations=2): @@ -386,31 +382,32 @@ def calibrate_dome_encoder_counts(self, num_cal_rotations=2): Number of rotations to perform to calibrate encoder. """ - if self._move_event.is_set(): - logger.debug('Movement command in progress') - raise Exception("Movement command in progress") + if self.movement_thread_active: + logger.warning('Movement command in progress') + return # rotate the dome until we hit home, to give reference point logger.debug(f'calibrate_dome_encoder_counts: Finding Home') self.find_home() - while self._move_event.is_set(): + while self.movement_thread_active: # wait for find_home() to finish time.sleep(0.1) logger.debug(f'calibrate_dome_encoder_counts: Found Home') # pause to let things settle/get a noticeable blink of debug_lights time.sleep(0.5) - # reset rotation_count instance variable to zero - self.rotation_count = 0 - self.num_cal_rotations = num_cal_rotations + # instance variable to track rotations during calibration + self._rotation_count = 0 + # instance variable that is over written in calibrate routine + self._num_cal_rotations = num_cal_rotations # now set dome to rotate num_cal_rotations times so we can determine # the number of ticks per revolution logger.debug(f'calibrate_dome_encoder_counts: Starting calibration rotations.') self._move_event.set() self._rotate_dome(Direction.CW) self._calibrating = True - calibrate = threading.Thread(target=self._calibrate) + calibrate = threading.Thread(target=self._simulate_calibration) cal_monitor = threading.Thread(target=self._thread_condition, - args=(self._calibration_completed,)) + args=(self._calibration_complete,)) calibrate.start() cal_monitor.start() @@ -419,16 +416,16 @@ def find_home(self): Move Dome to home position. """ - if self._move_event.is_set(): - logger.debug('Movement command in progress') - raise Exception("Movement command in progress") + if self.movement_thread_active: + logger.warning('Movement command in progress') + return # iniate the movement and set the _move_event flag logger.debug(f'find_home: Finding Home') self._move_event.set() self._rotate_dome(Direction.CW) time.sleep(0.1) homing = threading.Thread(target=self._thread_condition, - args=(self._find_home_condition,)) + args=(self._find_home_complete,)) homing.start() if self.testing: # in testing mode need to "fake" the activation of the home pin @@ -442,7 +439,7 @@ def sync(self, az): Parameters ---------- az : float - Desired dome azimuth position in degrees. + Current dome azimuth position in degrees. """ logger.debug(f'sync: syncing encoder counts to azimuth [{az}]') @@ -453,7 +450,7 @@ def sync(self, az): # Private Methods ############################################################################### - def _thread_condition(self, trigger_condition): + def _thread_condition(self, trigger_condition, *args, **kwargs): """Condition monitoring thread for movement commands. Will return when: @@ -462,15 +459,20 @@ def _thread_condition(self, trigger_condition): - thread running time exceeds self.wait_timeout """ + calibration_success = False + # This will update to false, if while loop is terminated by + # trigger_condition() + # TODO: better system for flagging success/abort/timeout goingtoaz = False - if trigger_condition.__name__ == '_goto_az_condition': + if trigger_condition.__name__ == '_goto_az_complete': goingtoaz = True - start = time.time() + start = time.monotonic() + while True: - time.sleep(0.1) - wait_time = time.time() - start - if trigger_condition(): + wait_time = time.monotonic() - start + if trigger_condition(*args, **kwargs): logger.debug(f'Monitoring thread triggered by {trigger_condition.__name__}') + calibration_success = True break elif self._abort_event.is_set(): logger.debug(f'Monitoring thread triggered by abort event') @@ -481,15 +483,22 @@ def _thread_condition(self, trigger_condition): elif goingtoaz and self.testing is True: # if testing simulate a tick for every cycle of while loop self._simulate_ticks(num_ticks=1) + time.sleep(0.1) + logger.debug('Monitor Thread: Stopping dome movement') self._stop_moving() + + if trigger_condition.__name__ == '_calibration_complete': + # set the azimuth per encoder tick factor based on + # how many ticks we counted over n rotations + self._degrees_per_tick = Angle(360 / (self.encoder_count / + self._rotation_count) * u.deg) # reset various dome state variables/events self._move_event.clear() self._calibrating = False - self.rotation_count = 0 return - def _goto_az_condition(self): + def _goto_az_complete(self, target_az): """Determines if current azimuth is within tolerance of target azimuth. Returns @@ -498,47 +507,37 @@ def _goto_az_condition(self): Returns true if self.dome_az is within tolerance of target azimuth. """ - delta_az = self.current_direction * (self.target_az - self.dome_az).wrap_at('180d') - logger.debug(f'Delta_az is {delta_az}') + raw_delta_az = (target_az - self.dome_az).wrap_at('180d') + delta_az = self.current_direction * raw_delta_az + logger.debug(f'Delta_az is {delta_az}, tolerance window is {self.az_position_tolerance}') return delta_az <= self.az_position_tolerance - def _find_home_condition(self): + def _find_home_complete(self): """Return True if the dome is at home.""" - # need a callable method for _thread_condition return self._home_sensor.is_active - def _calibration_completed(self): - """Return True if both calibrating and move_event flags are True""" - cal_in_progress = self._calibrating and self._move_event.is_set() - return not cal_in_progress + def _calibration_complete(self): + """Return True if desired number of calibration rotations completed.""" + logger.debug(f'cc Rotation count is [{self._rotation_count}], rotations to go [{self._num_cal_rotations - self._rotation_count}].') + return self._rotation_count >= self._num_cal_rotations - def _calibrate(self): + def _simulate_calibration(self): """Calibration method to be called from within a non-blocking thread""" - start = time.time() - # if self.testing: - # for i in range(self.num_cal_rotations): - # pass + start = time.monotonic() first_rotation = threading.Thread(target=self._simulate_rotation) first_rotation.start() while True: - logger.debug(f'Loop {time.time() - start}: rot_count [{self.rotation_count}], encoder_count [{self.encoder_count}]') - logger.debug(f'Simulated rotation underway {self._simulated_rotation_event.is_set()}') - if self.testing and not self._simulated_rotation_event.is_set(): + logger.debug(f'Loop {time.monotonic() - start}: rot_count [{self._rotation_count}], encoder_count [{self.encoder_count}]') + if not self._simulated_rotation_event.is_set(): + time.sleep(1) logger.debug(f'Calibration monitor thread: Completion of simulated rotation detected, simulating next rotation.') simulated_rotation = threading.Thread(target=self._simulate_rotation) - if self.rotation_count >= self.num_cal_rotations: - # if we have completed the number of calibration rotations, - # stop dome rotation. + if self._calibration_complete(): logger.debug(f'Calibration monitor thread: calibration rotations completed, ending calibration.') - self._stop_moving() break simulated_rotation.start() time.sleep(1) - # set the azimuth per encoder tick factor based on how many ticks we - # counted over n rotations - self._degrees_per_tick = Angle( - 360 / (self.encoder_count / self.rotation_count) * u.deg) def _set_at_home(self): """ @@ -555,7 +554,7 @@ def _set_at_home(self): # if we are calibrating, increment the rotation count if self._calibrating: logger.debug(f'_set_at_home: Home triggered during calibration, incrementing calibration rotation count.') - self.rotation_count += 1 + self._rotation_count += 1 def _set_not_home(self): """ From df8a9dd4493fc35c2eff2a67ebe44bac5a7ea41c Mon Sep 17 00:00:00 2001 From: fergus Date: Thu, 17 Oct 2019 22:15:14 +1100 Subject: [PATCH 7/7] tiny changes --- domehunter/dome_control.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/domehunter/dome_control.py b/domehunter/dome_control.py index a9ca51f..1c6ab9e 100644 --- a/domehunter/dome_control.py +++ b/domehunter/dome_control.py @@ -335,7 +335,6 @@ def abort(self): # wait for threads to abort while self.movement_thread_active: time.sleep(0.1) - pass self._abort_event.clear() def goto_az(self, az): @@ -436,6 +435,7 @@ def find_home(self): def sync(self, az): """ Sync encoder count to azimuth position. + Parameters ---------- az : float @@ -458,6 +458,11 @@ def _thread_condition(self, trigger_condition, *args, **kwargs): - abort event is triggered - thread running time exceeds self.wait_timeout + Parameters + ---------- + trigger_condition : callable method + Callable method that returns a boolean value. + """ calibration_success = False # This will update to false, if while loop is terminated by