From d3899d9d8b19ab80b498b359fb73b2cdd300324b Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 16 Jul 2019 02:04:31 +1000 Subject: [PATCH 01/18] rough implementation --- domehunter/__init__.py | 146 +++++++++++++++++++++++++++++++---------- setup.cfg | 2 +- 2 files changed, 114 insertions(+), 34 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index edd0863..d5573e7 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -5,8 +5,9 @@ # Packages may add whatever they like to this file, but # should keep this content at the top. # ---------------------------------------------------------------------------- -from gpiozero import DigitalInputDevice, Device +from gpiozero import DigitalInputDevice, DigitalOutputDevice, Device from gpiozero.pins.mock import MockFactory +import time from ._astropy_init import * # ---------------------------------------------------------------------------- @@ -31,14 +32,7 @@ class UnsupportedPythonError(Exception): pass -class X2DomeRPC(): - """Dummy class until real RPC class is available.""" - - def __init__(self, *args, **kwargs): - """Create dummy class until real RPC class is available.""" - - -class Dome(X2DomeRPC): +class Dome(): """ Interface to dome control raspberry pi GPIO. @@ -57,23 +51,35 @@ def __init__(self, testing=True, *args, **kwargs): else: """ Do not change until you're sure!!! """ # https://pinout.xyz/pinout/automation_hat - ENCODER_PIN_NUMBER = None - HOME_SENSOR_PIN_NUMBER = None + # input 1 on automation hat + ENCODER_PIN_NUMBER = 26 + # input 2 on the automation hat + HOME_SENSOR_PIN_NUMBER = 20 + # relay 1 on automation hat + ROTATION_RELAY_PIN_NUMBER = 13 + # relay 2 on automation hat + # on position for CW and off for CCW? + DIRECTION_RELAY_PIN_NUMBER = 19 self.dome_status = "unknown" + self.dome_az = None self.encoder_count = 0 - self.encoder = DigitalInputDevice(ENCODER_PIN_NUMBER) + self.encoder = DigitalInputDevice(ENCODER_PIN_NUMBER, bounce_time=0.1) # _increment_count function to run when encoder is triggered self.encoder.when_activated = self._increment_count + self.az_per_tick = 1 self._at_home = False - self.home_sensor = DigitalInputDevice(HOME_SENSOR_PIN_NUMBER) + self.home_sensor = DigitalInputDevice(HOME_SENSOR_PIN_NUMBER, bounce_time=0.1) # _set_not_home function is run when home sensor is NOT being triggered self.home_sensor.when_deactivated = self._set_not_home # _set_at_home function is run when home sensor is triggered self.home_sensor.when_activated = self._set_at_home + self.rotation_relay = DigitalOutputDevice(ROTATION_RELAY_PIN_NUMBER, initial_value=False) + self.direction_relay = DigitalOutputDevice(DIRECTION_RELAY_PIN_NUMBER, initial_value=False) + ################################################################################################## # Properties ################################################################################################## @@ -88,7 +94,6 @@ def status(self): """Return a text string describing dome rotators current status.""" pass - ################################################################################################## # Methods ################################################################################################## @@ -99,17 +104,80 @@ def abort(self): """Stop everything.""" pass - def getAzEl(self): - """Return AZ and Elevation.""" + def getAz(self): + """Return AZ.""" + if self.dome_az is None: + self.calibrate() + return self.dome_az + + def GotoAz(self, az): + "Send Dome to Az." + if self.dome_az is None: + self.calibrate() + delta_az = az - self.dome_az + if abs(delta_az) > 180: + if delta_az > 0: + delta_az -= 360 + else: + delta_az += 360 + + if delta_az > 0: + ticks = self._az_to_ticks(delta_az) + target_position = self.encoder_count + ticks + self._move_cw() + while self.encoder_count <= target_position: + pass + self._stop_moving() + # compare original count to current, just in case we got more ticks than we asked for + old_encoder_count = target_position - ticks + self.dome_az += self._ticks_to_az(self.encoder_count - old_encoder_count) + self.dome_az %= 360 + self.encoder_count = self._az_to_ticks(self.dome_az) + pass + if delta_az < 0: + # here ticks is going to be negative + ticks = self._az_to_ticks(delta_az) + target_position = self.encoder_count + ticks + self._move_ccw() + while self.encoder_count >= target_position: + pass + self._stop_moving() + # compare original count to current, just in case we got more ticks than we asked for + old_encoder_count = target_position - ticks + self.dome_az += self._ticks_to_az(self.encoder_count - old_encoder_count) + self.dome_az %= 360 + self.encoder_count = self._az_to_ticks(self.dome_az) + pass pass - def start_daemon(self): - """Maybe start the RCP daemon here?.""" - raise NotImplementedError + def calibrate(self): + """Calibrate the encoder (determine degrees per tick)""" + self._move_cw() + self.home_sensor.wait_for_active() + time.sleep(0.5) + self._stop_moving() + self.encoder_count = 0 + time.sleep(0.5) + self._move_cw() + # this might be dumb but trying to get two rotations worth of encoder ticks + self.home_sensor.wait_for_active() + time.sleep(0.5) + self._stop_moving() + time.sleep(0.5) + self._move_cw() + self.home_sensor.wait_for_active() + time.sleep(0.5) + self._stop_moving() + self.az_per_tick = 360 / (self.encoder_count / 2) + pass - def halt_daemon(self): - """Maybe start the RCP daemon here?.""" - raise NotImplementedError + # def start_daemon(self): + # """Maybe start the RCP daemon here?.""" + # raise NotImplementedError + # + # def halt_daemon(self): + # """Maybe start the RCP daemon here?.""" + # raise NotImplementedError ################################################################################################## # Private Methods @@ -133,23 +201,35 @@ def _increment_count(self, device=None): elif self.last_direction == "CCW": self.encoder_count -= 1 - def _move_west(self, degrees_west): - print(f"Moving {degrees_west} West") - if self._west_is_ccw: - cmd_status = self.move_ccw(degrees_west) - else: - cmd_status = self.move_cw(degrees_west) + def _az_to_ticks(self, az): + return az / self.az_per_tick - return cmd_status + def _ticks_to_az(self, ticks): + return ticks * self.az_per_tick - def _move_cw(self, degrees): - print(f"Sending GPIO move_cw({degrees}) command.") + + + def _move_cw(self): + # print(f"Sending GPIO move_cw({degrees}) command.") self.current_direction = "CW" + # set the direction relay switch to CW position + self.direction_relay.on() + # turn on rotation + self.rotation_relay.on() cmd_status = True return cmd_status - def _move_ccw(self, degrees): - print(f"Sending GPIO move_ccw({degrees}) command.") + def _move_ccw(self): + # print(f"Sending GPIO move_ccw({degrees}) command.") self.current_direction = "CCW" + # set the direction relay switch to CCW position + self.direction_relay.off() + # turn on rotation + self.rotation_relay.on() cmd_status = True return cmd_status + + def _stop_moving(self): + self.rotation_relay.off() + self.last_direction = self.current_direction + self.current_direction = None diff --git a/setup.cfg b/setup.cfg index 89049a8..d72a667 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,4 +43,4 @@ install_requires = astropy # version should be PEP440 compatible (https://www.python.org/dev/peps/pep-0440/) version = 0.0.dev # Note: you will also need to change this in your package's __init__.py -minimum_python_version = 3.7 +minimum_python_version = 3.6 From 38d3da735e8f575d8ed74447d8dd29adf37c0d14 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 16 Jul 2019 14:40:45 +1000 Subject: [PATCH 02/18] added a few comments/autopep8 --- domehunter/__init__.py | 100 +++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index d5573e7..4cf6edf 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -1,21 +1,20 @@ """Run dome control on a raspberry pi GPIO.""" # Licensed under a 3-clause BSD style license - see LICENSE.rst - -# Packages may add whatever they like to this file, but # should keep this content at the top. # ---------------------------------------------------------------------------- -from gpiozero import DigitalInputDevice, DigitalOutputDevice, Device -from gpiozero.pins.mock import MockFactory +import sys import time +from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice +from gpiozero.pins.mock import MockFactory + from ._astropy_init import * + # ---------------------------------------------------------------------------- # Enforce Python version check during package import. # This is the same check as the one at the top of setup.py -import sys - __minimum_python_version__ = "3.6" @@ -36,8 +35,10 @@ class Dome(): """ Interface to dome control raspberry pi GPIO. - Might start with: https://gpiozero.readthedocs.io/en/stable/ - but might use something else if we buy a fancy Pi HAT. + To see the gpio pin mapping to the automation HAT see here, + https://pinout.xyz/pinout/automation_hat + For infomation on the gpiozero library see here, + https://gpiozero.readthedocs.io/en/stable/ """ def __init__(self, testing=True, *args, **kwargs): @@ -46,8 +47,26 @@ def __init__(self, testing=True, *args, **kwargs): # Set the default pin factory to a mock factory Device.pin_factory = MockFactory() - ENCODER_PIN_NUMBER = 7 - HOME_SENSOR_PIN_NUMBER = 11 + ENCODER_PIN_NUMBER = 26 + # input 2 on the automation hat + HOME_SENSOR_PIN_NUMBER = 20 + # relay 1 on automation hat + ROTATION_RELAY_PIN_NUMBER = 13 + # relay 2 on automation hat + # on position for CW and off for CCW? + DIRECTION_RELAY_PIN_NUMBER = 19 + # NB I'm just going to move this entirely into a tests file + # to create test device just do the following + """ + encoder = DigitialInputDevice(ENCODER_PIN_NUMBER, bounce_time=0.1) + # create reference to the mock pin used by the device + encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) + # then toggle the pin high/low to simulate encoder ticks/home home + # sensor or activation of the relays. + encoder_pin.drive_low() + time.sleep(0.1) + encoder_pin.drive_high() + """ else: """ Do not change until you're sure!!! """ # https://pinout.xyz/pinout/automation_hat @@ -61,28 +80,43 @@ def __init__(self, testing=True, *args, **kwargs): # on position for CW and off for CCW? DIRECTION_RELAY_PIN_NUMBER = 19 + # initialize status and az as unknown, to ensure we have properly + # calibrated az self.dome_status = "unknown" self.dome_az = None + # create a instance variable to track the dome motor encoder ticks self.encoder_count = 0 + # bounce_time settings gives the time in seconds that the device will + # ignore additional activation signals self.encoder = DigitalInputDevice(ENCODER_PIN_NUMBER, bounce_time=0.1) # _increment_count function to run when encoder is triggered self.encoder.when_activated = self._increment_count + # dummy value self.az_per_tick = 1 self._at_home = False - self.home_sensor = DigitalInputDevice(HOME_SENSOR_PIN_NUMBER, bounce_time=0.1) - # _set_not_home function is run when home sensor is NOT being triggered + self.home_sensor = DigitalInputDevice( + HOME_SENSOR_PIN_NUMBER, bounce_time=0.1) + # _set_not_home function is run when upon home senser deactivation self.home_sensor.when_deactivated = self._set_not_home - # _set_at_home function is run when home sensor is triggered + # _set_at_home function is run when home sensor is activated self.home_sensor.when_activated = self._set_at_home - self.rotation_relay = DigitalOutputDevice(ROTATION_RELAY_PIN_NUMBER, initial_value=False) - self.direction_relay = DigitalOutputDevice(DIRECTION_RELAY_PIN_NUMBER, initial_value=False) - -################################################################################################## + # these two DODs control the relays that control the dome motor + # the rotation relay is the on/off switch for dome rotation + # the direction relay will toggle either the CW or CCW direction + # (using both the normally open and normally close relay terminals)\ + # so when moving the dome, first set the direction relay position + # then activate the rotation relay + self.rotation_relay = DigitalOutputDevice( + ROTATION_RELAY_PIN_NUMBER, initial_value=False) + self.direction_relay = DigitalOutputDevice( + DIRECTION_RELAY_PIN_NUMBER, initial_value=False) + +############################################################################### # Properties -################################################################################################## +############################################################################### @property def is_home(self): @@ -94,14 +128,15 @@ def status(self): """Return a text string describing dome rotators current status.""" pass -################################################################################################## +############################################################################### # Methods -################################################################################################## +############################################################################### """These map directly onto the AbstractMethods created by RPC.""" def abort(self): """Stop everything.""" + # this could simply call the self._stop_moving() method? pass def getAz(self): @@ -128,9 +163,11 @@ def GotoAz(self, az): while self.encoder_count <= target_position: pass self._stop_moving() - # compare original count to current, just in case we got more ticks than we asked for + # compare original count to current just in case we got more ticks + # than we asked for old_encoder_count = target_position - ticks - self.dome_az += self._ticks_to_az(self.encoder_count - old_encoder_count) + self.dome_az += self._ticks_to_az( + self.encoder_count - old_encoder_count) self.dome_az %= 360 self.encoder_count = self._az_to_ticks(self.dome_az) pass @@ -142,9 +179,12 @@ def GotoAz(self, az): while self.encoder_count >= target_position: pass self._stop_moving() - # compare original count to current, just in case we got more ticks than we asked for + # compare original count to current, just in case we got more ticks + # than we asked for + old_encoder_count = target_position - ticks - self.dome_az += self._ticks_to_az(self.encoder_count - old_encoder_count) + self.dome_az += self._ticks_to_az( + self.encoder_count - old_encoder_count) self.dome_az %= 360 self.encoder_count = self._az_to_ticks(self.dome_az) pass @@ -159,7 +199,7 @@ def calibrate(self): self.encoder_count = 0 time.sleep(0.5) self._move_cw() - # this might be dumb but trying to get two rotations worth of encoder ticks + # might be dumb but trying to get two rotations worth of encoder ticks self.home_sensor.wait_for_active() time.sleep(0.5) self._stop_moving() @@ -171,6 +211,9 @@ def calibrate(self): self.az_per_tick = 360 / (self.encoder_count / 2) pass + # This will be dome the gRPC python server implementation, which will + # create an instance of this dome class + # # def start_daemon(self): # """Maybe start the RCP daemon here?.""" # raise NotImplementedError @@ -179,9 +222,9 @@ def calibrate(self): # """Maybe start the RCP daemon here?.""" # raise NotImplementedError -################################################################################################## +############################################################################### # Private Methods -################################################################################################## +############################################################################### def _set_at_home(self): self._at_home = True @@ -190,6 +233,7 @@ def _set_not_home(self): self._at_home = False def _increment_count(self, device=None): + # Unsure what purpose the device variable is supposed to serve here? print(f"{device} activated _increment_count") if self.current_direction == "CW": self.encoder_count += 1 @@ -207,8 +251,6 @@ def _az_to_ticks(self, az): def _ticks_to_az(self, ticks): return ticks * self.az_per_tick - - def _move_cw(self): # print(f"Sending GPIO move_cw({degrees}) command.") self.current_direction = "CW" From 1f394a7dafade6d06cfd7fcaf35815a983e75b81 Mon Sep 17 00:00:00 2001 From: fergus Date: Thu, 18 Jul 2019 19:16:42 +1000 Subject: [PATCH 03/18] added a testing mode --- domehunter/__init__.py | 124 +++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index 4cf6edf..e589cda 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -55,21 +55,14 @@ def __init__(self, testing=True, *args, **kwargs): # relay 2 on automation hat # on position for CW and off for CCW? DIRECTION_RELAY_PIN_NUMBER = 19 - # NB I'm just going to move this entirely into a tests file - # to create test device just do the following - """ - encoder = DigitialInputDevice(ENCODER_PIN_NUMBER, bounce_time=0.1) - # create reference to the mock pin used by the device - encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) - # then toggle the pin high/low to simulate encoder ticks/home home - # sensor or activation of the relays. - encoder_pin.drive_low() - time.sleep(0.1) - encoder_pin.drive_high() - """ + # set a timeout length in seconds for wait_for_active() calls + WAIT_TIMEOUT = 1 + # set a variable for bounce_time in seconds + BOUNCE_TIME = 0.1 + + self.encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) else: """ Do not change until you're sure!!! """ - # https://pinout.xyz/pinout/automation_hat # input 1 on automation hat ENCODER_PIN_NUMBER = 26 # input 2 on the automation hat @@ -79,17 +72,24 @@ def __init__(self, testing=True, *args, **kwargs): # relay 2 on automation hat # on position for CW and off for CCW? DIRECTION_RELAY_PIN_NUMBER = 19 + # set the timeout length variable to None for non testing mode + WAIT_TIMEOUT = None + # set a variable for bounce_time in seconds + BOUNCE_TIME = 0.1 # initialize status and az as unknown, to ensure we have properly # calibrated az + self.testing = testing self.dome_status = "unknown" self.dome_az = None + self.current_direction = None # create a instance variable to track the dome motor encoder ticks self.encoder_count = 0 # bounce_time settings gives the time in seconds that the device will # ignore additional activation signals - self.encoder = DigitalInputDevice(ENCODER_PIN_NUMBER, bounce_time=0.1) + self.encoder = DigitalInputDevice( + ENCODER_PIN_NUMBER, bounce_time=BOUNCE_TIME) # _increment_count function to run when encoder is triggered self.encoder.when_activated = self._increment_count # dummy value @@ -97,7 +97,7 @@ def __init__(self, testing=True, *args, **kwargs): self._at_home = False self.home_sensor = DigitalInputDevice( - HOME_SENSOR_PIN_NUMBER, bounce_time=0.1) + HOME_SENSOR_PIN_NUMBER, bounce_time=BOUNCE_TIME) # _set_not_home function is run when upon home senser deactivation self.home_sensor.when_deactivated = self._set_not_home # _set_at_home function is run when home sensor is activated @@ -114,6 +114,11 @@ def __init__(self, testing=True, *args, **kwargs): self.direction_relay = DigitalOutputDevice( DIRECTION_RELAY_PIN_NUMBER, initial_value=False) + # set a wait time for testing mode that exceeds BOUNCE_TIME + self.t_wait = BOUNCE_TIME + 0.05 + # set the timeout for wait_for_active() + self.wait_timeout = WAIT_TIMEOUT + ############################################################################### # Properties ############################################################################### @@ -136,7 +141,7 @@ def status(self): def abort(self): """Stop everything.""" - # this could simply call the self._stop_moving() method? + self._stop_moving() pass def getAz(self): @@ -150,77 +155,89 @@ def GotoAz(self, az): if self.dome_az is None: self.calibrate() delta_az = az - self.dome_az + + # determine whether CW or CCW gives the short path to desired az if abs(delta_az) > 180: if delta_az > 0: delta_az -= 360 else: delta_az += 360 + # if updated delta_az is positive, direction is CW if delta_az > 0: + # converted delta_az to equivilant in encoder ticks ticks = self._az_to_ticks(delta_az) target_position = self.encoder_count + ticks self._move_cw() - while self.encoder_count <= target_position: + # wait until encoder count matches desired delta az + while self.encoder_count < target_position: + if self.testing: + # if testing simulate a tick for every cycle of while loop + self._simulate_ticks(num_ticks=1) pass self._stop_moving() # compare original count to current just in case we got more ticks # than we asked for old_encoder_count = target_position - ticks + # update dome_az based on the actual number of ticks counted self.dome_az += self._ticks_to_az( self.encoder_count - old_encoder_count) + # take mod360 of dome_az to keep 0 <= dome_az < 360 self.dome_az %= 360 + # update encoder_count to match the dome_az self.encoder_count = self._az_to_ticks(self.dome_az) pass + + # if updated delta_az is negative, direction is CCW if delta_az < 0: - # here ticks is going to be negative + # converted delta_az to equivilant in encoder ticks ticks = self._az_to_ticks(delta_az) target_position = self.encoder_count + ticks self._move_ccw() + # wait until encoder count matches desired delta az while self.encoder_count >= target_position: + if self.testing: + # if testing simulate a tick for every cycle of while loop + self._simulate_ticks(num_ticks=1) pass self._stop_moving() # compare original count to current, just in case we got more ticks # than we asked for - old_encoder_count = target_position - ticks + # update dome_az based on the actual number of ticks counted self.dome_az += self._ticks_to_az( self.encoder_count - old_encoder_count) + # take mod360 of dome_az to keep 0 <= dome_az < 360 self.dome_az %= 360 + # update encoder_count to match the dome_az self.encoder_count = self._az_to_ticks(self.dome_az) pass pass - def calibrate(self): + def calibrate(self, num_cal_rotations=2): """Calibrate the encoder (determine degrees per tick)""" + # rotate the dome until we hit home, to give reference point self._move_cw() - self.home_sensor.wait_for_active() - time.sleep(0.5) + self.home_sensor.wait_for_active(timeout=self.wait_timeout) + time.sleep(0.1) self._stop_moving() self.encoder_count = 0 - time.sleep(0.5) - self._move_cw() - # might be dumb but trying to get two rotations worth of encoder ticks - self.home_sensor.wait_for_active() - time.sleep(0.5) - self._stop_moving() - time.sleep(0.5) - self._move_cw() - self.home_sensor.wait_for_active() - time.sleep(0.5) - self._stop_moving() - self.az_per_tick = 360 / (self.encoder_count / 2) - pass - # This will be dome the gRPC python server implementation, which will - # create an instance of this dome class - # - # def start_daemon(self): - # """Maybe start the RCP daemon here?.""" - # raise NotImplementedError - # - # def halt_daemon(self): - # """Maybe start the RCP daemon here?.""" - # raise NotImplementedError + # now set dome to rotate n times so we can determine the number of + # ticks per revolution + rotation_count = 0 + while rotation_count < num_cal_rotations: + time.sleep(0.5) + self._move_cw() + if self.testing: + self._simulate_ticks(num_ticks=10) + self.home_sensor.wait_for_active(timeout=self.wait_timeout) + time.sleep(0.5) + self._stop_moving() + rotation_count += 1 + + self.az_per_tick = 360 / (self.encoder_count / rotation_count) + pass ############################################################################### # Private Methods @@ -232,9 +249,9 @@ def _set_at_home(self): def _set_not_home(self): self._at_home = False - def _increment_count(self, device=None): + def _increment_count(self): # Unsure what purpose the device variable is supposed to serve here? - print(f"{device} activated _increment_count") + print(f"Encoder activated _increment_count") if self.current_direction == "CW": self.encoder_count += 1 elif self.current_direction == "CCW": @@ -252,7 +269,7 @@ def _ticks_to_az(self, ticks): return ticks * self.az_per_tick def _move_cw(self): - # print(f"Sending GPIO move_cw({degrees}) command.") + self.last_direction = self.current_direction self.current_direction = "CW" # set the direction relay switch to CW position self.direction_relay.on() @@ -262,7 +279,7 @@ def _move_cw(self): return cmd_status def _move_ccw(self): - # print(f"Sending GPIO move_ccw({degrees}) command.") + self.last_direction = self.current_direction self.current_direction = "CCW" # set the direction relay switch to CCW position self.direction_relay.off() @@ -275,3 +292,12 @@ def _stop_moving(self): self.rotation_relay.off() self.last_direction = self.current_direction self.current_direction = None + + def _simulate_ticks(self, num_ticks): + tick_count = 0 + while tick_count < num_ticks: + self.encoder_pin.drive_low() + time.sleep(self.t_wait) + self.encoder_pin.drive_high() + time.sleep(self.t_wait) + tick_count += 1 From 4d269635a304441a190be9a32543e5ff672fed8f Mon Sep 17 00:00:00 2001 From: fergus Date: Fri, 19 Jul 2019 04:05:11 +1000 Subject: [PATCH 04/18] added debug light mode f --- domehunter/__init__.py | 93 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index e589cda..b51cdfe 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -8,7 +8,10 @@ from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice from gpiozero.pins.mock import MockFactory - +# if we want to use the automation hat status lights we need to +# import the pimoroni led driver +import sn3218 +sn3218.disable() from ._astropy_init import * # ---------------------------------------------------------------------------- @@ -41,12 +44,12 @@ class Dome(): https://gpiozero.readthedocs.io/en/stable/ """ - def __init__(self, testing=True, *args, **kwargs): + def __init__(self, testing=True, debug_lights=False, *args, **kwargs): """Initialize raspberry pi GPIO environment.""" if testing: # Set the default pin factory to a mock factory Device.pin_factory = MockFactory() - + # input 1 on automation hat ENCODER_PIN_NUMBER = 26 # input 2 on the automation hat HOME_SENSOR_PIN_NUMBER = 20 @@ -61,6 +64,7 @@ def __init__(self, testing=True, *args, **kwargs): BOUNCE_TIME = 0.1 self.encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) + self.home_sensor_pin = Device.pin_factory.pin(HOME_SENSOR_PIN_NUMBER) else: """ Do not change until you're sure!!! """ # input 1 on automation hat @@ -70,19 +74,46 @@ def __init__(self, testing=True, *args, **kwargs): # relay 1 on automation hat ROTATION_RELAY_PIN_NUMBER = 13 # relay 2 on automation hat - # on position for CW and off for CCW? + # no position for CW and nc for CCW? DIRECTION_RELAY_PIN_NUMBER = 19 # set the timeout length variable to None for non testing mode WAIT_TIMEOUT = None # set a variable for bounce_time in seconds BOUNCE_TIME = 0.1 + if debug_lights: + sn3218.enable() + self.led_lights_id = { + 'power': 0b100000000000000000, + 'comms': 0b010000000000000000, + 'warn': 0b001000000000000000, + 'input_1': 0b000100000000000000, + 'input_2': 0b000010000000000000, + 'input_3': 0b000001000000000000, + 'relay_3_nc': 0b000000100000000000, + 'relay_3_no': 0b000000010000000000, + 'relay_2_nc': 0b000000001000000000, + 'relay_2_no': 0b000000000100000000, + 'relay_1_nc': 0b000000000010000000, + 'relay_1_no': 0b000000000001000000, + 'output_3': 0b000000000000100000, + 'output_2': 0b000000000000010000, + 'output_1': 0b000000000000001000, + 'adc_3': 0b000000000000000100, + 'adc_2': 0b000000000000000010, + 'adc_1': 0b000000000000000001 + } + # led_status is binary number, each zero/position sets the state + # of an LED, where 0 is off and 1 is on + self.led_status = 0b000000000000000000 + sn3218.enable_leds(self.led_status) # initialize status and az as unknown, to ensure we have properly # calibrated az self.testing = testing + self.debug_lights = debug_lights self.dome_status = "unknown" self.dome_az = None - self.current_direction = None + # create a instance variable to track the dome motor encoder ticks self.encoder_count = 0 @@ -113,6 +144,11 @@ def __init__(self, testing=True, *args, **kwargs): ROTATION_RELAY_PIN_NUMBER, initial_value=False) self.direction_relay = DigitalOutputDevice( DIRECTION_RELAY_PIN_NUMBER, initial_value=False) + # because we initiliase the relay in the nc position + self.current_direction = "CCW" + if debug_lights: + self._turn_led_on(leds=['relay_2_nc']) + self._turn_led_on(leds=['relay_1_nc']) # set a wait time for testing mode that exceeds BOUNCE_TIME self.t_wait = BOUNCE_TIME + 0.05 @@ -219,6 +255,8 @@ def calibrate(self, num_cal_rotations=2): # rotate the dome until we hit home, to give reference point self._move_cw() self.home_sensor.wait_for_active(timeout=self.wait_timeout) + if self.testing: + self.home_sensor_pin.drive_high() time.sleep(0.1) self._stop_moving() self.encoder_count = 0 @@ -230,10 +268,14 @@ def calibrate(self, num_cal_rotations=2): time.sleep(0.5) self._move_cw() if self.testing: + self.home_sensor_pin.drive_low() self._simulate_ticks(num_ticks=10) self.home_sensor.wait_for_active(timeout=self.wait_timeout) + if self.testing: + self.home_sensor_pin.drive_high() time.sleep(0.5) self._stop_moving() + rotation_count += 1 self.az_per_tick = 360 / (self.encoder_count / rotation_count) @@ -244,14 +286,19 @@ def calibrate(self, num_cal_rotations=2): ############################################################################### def _set_at_home(self): + self._turn_led_on(leds=['input_2']) self._at_home = True def _set_not_home(self): + self._turn_led_off(leds=['input_2']) self._at_home = False def _increment_count(self): # Unsure what purpose the device variable is supposed to serve here? print(f"Encoder activated _increment_count") + self._turn_led_on(leds=['input_1']) + time.sleep(0.01) + self._turn_led_off(leds=['input_1']) if self.current_direction == "CW": self.encoder_count += 1 elif self.current_direction == "CCW": @@ -269,29 +316,44 @@ def _ticks_to_az(self, ticks): return ticks * self.az_per_tick def _move_cw(self): + if self.testing and self._at_home: + self.home_sensor_pin.drive_low() self.last_direction = self.current_direction self.current_direction = "CW" # set the direction relay switch to CW position self.direction_relay.on() + if self.last_direction == "CCW": + self._turn_led_off(leds=['relay_2_nc']) + self._turn_led_on(leds=['relay_2_no']) # turn on rotation self.rotation_relay.on() + self._turn_led_on(leds=['relay_1_no']) + self._turn_led_off(leds=['relay_1_nc']) cmd_status = True return cmd_status def _move_ccw(self): + if self.testing and self._at_home: + self.home_sensor_pin.drive_low() self.last_direction = self.current_direction self.current_direction = "CCW" # set the direction relay switch to CCW position self.direction_relay.off() + if self.last_direction == "CW": + self._turn_led_off(leds=['relay_2_no']) + self._turn_led_on(leds=['relay_2_nc']) # turn on rotation self.rotation_relay.on() + self._turn_led_on(leds=['relay_1_no']) + self._turn_led_off(leds=['relay_1_nc']) cmd_status = True return cmd_status def _stop_moving(self): self.rotation_relay.off() + self._turn_led_off(leds=['relay_1_no']) + self._turn_led_on(leds=['relay_1_nc']) self.last_direction = self.current_direction - self.current_direction = None def _simulate_ticks(self, num_ticks): tick_count = 0 @@ -301,3 +363,22 @@ def _simulate_ticks(self, num_ticks): self.encoder_pin.drive_high() time.sleep(self.t_wait) tick_count += 1 + + def _turn_led_on(self, leds=[]): + # pass a list of strings of the leds to turn on + if self.debug_lights: + for led in leds: + self.led_status |= self.led_lights_id[led] + sn3218.enable_leds(self.led_status) + pass + + def _turn_led_off(self, leds=[]): + # pass a list of strings of the leds to turn off + # note: need something to prevent us turning an LED off/on more than + # once in a row because it will affect the binary led_status in + # unintended ways + if self.debug_lights: + for led in leds: + self.led_status ^= self.led_lights_id[led] + sn3218.enable_leds(self.led_status) + pass From aad7a30cefdd89b4c3e7e88442dbacf2d6deb6c3 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 19 Jul 2019 15:10:58 +1000 Subject: [PATCH 05/18] changed led control method was using bitwise operations to update the binary int that sets which leds are on/off. Couldn't find a bitwise method to create a turn_off() that would behave as intended if called more than once. switched to just converting the binary int to a string and then a list, then updating the list by a dictionary that maps the led name to its index in the string/list. Finally convert the list to a string and back to a binary int. --- domehunter/__init__.py | 90 ++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index b51cdfe..cd01330 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -1,19 +1,20 @@ """Run dome control on a raspberry pi GPIO.""" + # Licensed under a 3-clause BSD style license - see LICENSE.rst -# should keep this content at the top. -# ---------------------------------------------------------------------------- import sys import time -from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice -from gpiozero.pins.mock import MockFactory # if we want to use the automation hat status lights we need to # import the pimoroni led driver import sn3218 -sn3218.disable() +from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice +from gpiozero.pins.mock import MockFactory + from ._astropy_init import * +sn3218.disable() + # ---------------------------------------------------------------------------- # Enforce Python version check during package import. @@ -64,7 +65,8 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): BOUNCE_TIME = 0.1 self.encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) - self.home_sensor_pin = Device.pin_factory.pin(HOME_SENSOR_PIN_NUMBER) + self.home_sensor_pin = Device.pin_factory.pin( + HOME_SENSOR_PIN_NUMBER) else: """ Do not change until you're sure!!! """ # input 1 on automation hat @@ -82,31 +84,33 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): BOUNCE_TIME = 0.1 if debug_lights: - sn3218.enable() - self.led_lights_id = { - 'power': 0b100000000000000000, - 'comms': 0b010000000000000000, - 'warn': 0b001000000000000000, - 'input_1': 0b000100000000000000, - 'input_2': 0b000010000000000000, - 'input_3': 0b000001000000000000, - 'relay_3_nc': 0b000000100000000000, - 'relay_3_no': 0b000000010000000000, - 'relay_2_nc': 0b000000001000000000, - 'relay_2_no': 0b000000000100000000, - 'relay_1_nc': 0b000000000010000000, - 'relay_1_no': 0b000000000001000000, - 'output_3': 0b000000000000100000, - 'output_2': 0b000000000000010000, - 'output_1': 0b000000000000001000, - 'adc_3': 0b000000000000000100, - 'adc_2': 0b000000000000000010, - 'adc_1': 0b000000000000000001 - } # led_status is binary number, each zero/position sets the state # of an LED, where 0 is off and 1 is on self.led_status = 0b000000000000000000 + sn3218.output([0x10] * 18) sn3218.enable_leds(self.led_status) + sn3218.enable() + self.led_lights_ind = { + 'power': 2, + 'comms': 3, + 'warn': 4, + 'input_1': 5, + 'input_2': 6, + 'input_3': 7, + 'relay_3_nc': 8, + 'relay_3_no': 9, + 'relay_2_nc': 10, + 'relay_2_no': 11, + 'relay_1_nc': 12, + 'relay_1_no': 13, + 'output_3': 14, + 'output_2': 15, + 'output_1': 16, + 'adc_3': 17, + 'adc_2': 18, + 'adc_1': 19 + } + # initialize status and az as unknown, to ensure we have properly # calibrated az self.testing = testing @@ -114,7 +118,6 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): self.dome_status = "unknown" self.dome_az = None - # create a instance variable to track the dome motor encoder ticks self.encoder_count = 0 # bounce_time settings gives the time in seconds that the device will @@ -367,18 +370,39 @@ def _simulate_ticks(self, num_ticks): def _turn_led_on(self, leds=[]): # pass a list of strings of the leds to turn on if self.debug_lights: + # this function needs a bunch of checks at some point + # like length of the binary number, whether things have the right + # type at the end etc etc + # + # take the current led_status and convert to a string in binary + # format (18bit) + new_state = format(self.led_status, '#020b') + # from that string create a list of characters + # use the keys in the leds list and the led_lights_ind + new_state = list(new_state) for led in leds: - self.led_status |= self.led_lights_id[led] + ind = self.led_lights_ind[led] + new_state[ind] = '1' + # convert the updated list to a string and then to a binary int + new_state = ''.join(new_state) + self.led_status = int(new_state, 2) sn3218.enable_leds(self.led_status) pass def _turn_led_off(self, leds=[]): # pass a list of strings of the leds to turn off - # note: need something to prevent us turning an LED off/on more than - # once in a row because it will affect the binary led_status in - # unintended ways if self.debug_lights: + # take the current led_status and convert to a string in binary + # format (18bit) + new_state = format(self.led_status, '#020b') + # from that string create a list of characters + # use the keys in the leds list and the led_lights_ind + new_state = list(new_state) for led in leds: - self.led_status ^= self.led_lights_id[led] + ind = self.led_lights_ind[led] + new_state[ind] = '0' + # convert the updated list to a string and then to a binary int + new_state = ''.join(new_state) + self.led_status = int(new_state, 2) sn3218.enable_leds(self.led_status) pass From 19e615c6d0241e1f84a7f17fb343da2fe40c8a04 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 19 Jul 2019 15:45:06 +1000 Subject: [PATCH 06/18] import error handling --- domehunter/__init__.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index cd01330..5581972 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -1,19 +1,29 @@ """Run dome control on a raspberry pi GPIO.""" -# Licensed under a 3-clause BSD style license - see LICENSE.rst import sys import time +import warnings -# if we want to use the automation hat status lights we need to -# import the pimoroni led driver -import sn3218 from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice from gpiozero.pins.mock import MockFactory from ._astropy_init import * -sn3218.disable() +# if we want to use the automation hat status lights we need to +# import the pimoroni led driver +try: + import sn3218 + sn3218.disable() +except OSError: + warnings.warn( + "AutomationHAT hardware not detected, testing=True and debug_lights=False recommended.") + pass +except: + warnings.warn( + "Something went wrong in importing sn3218, status lights unlikely to work.") + pass + # ---------------------------------------------------------------------------- From 41415f3d15792abf85ed1594d731d7ffba287eb8 Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 30 Jul 2019 15:08:23 +1000 Subject: [PATCH 07/18] comments/readme/requirements --- README.rst | 26 ++- domehunter/__init__.py | 126 ++++++++++++-- examples/dome_control_example.ipynb | 251 ++++++++++++++++++++++++++++ requirements.txt | 2 + 4 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 examples/dome_control_example.ipynb diff --git a/README.rst b/README.rst index 6e764bb..7d83b15 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,9 @@ and tracks the dome position using an encoder. It returns infomation The c++ code is built around Software Bisque's X2 standard. For more infomation on this `see here `. +C++/gRPC Component +================== + Requirements --------------- @@ -91,7 +94,25 @@ this as the generated files are committed to the repositry and shouldn't need to be generated. +Python RaspberryPi Component +============================ + +Requirements +--------------- +Required: +- `gpiozero` python library +Optional: +- `smbus` and `sn3218` python libraries + +Note: The `smbus` and `sn3218` are used to control the automationHAT status +LEDs. If you plan on running the code without the automationHAT these libraries +aren't required. +Getting Started +--------------- +Follow the example jupyter notebook in the examples direction +(`dome_control_example`). The automationHAT hardware is not required to run the +code in testing mode. License @@ -102,8 +123,3 @@ the terms of the BSD 3-Clause license. This package is based upon the `Astropy package template `_ which is licensed under the BSD 3-clause licence. See the licenses folder for more information. - - - - - diff --git a/domehunter/__init__.py b/domehunter/__init__.py index 5581972..22e4515 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -27,6 +27,7 @@ # ---------------------------------------------------------------------------- + # Enforce Python version check during package import. # This is the same check as the one at the top of setup.py __minimum_python_version__ = "3.6" @@ -53,10 +54,47 @@ class Dome(): https://pinout.xyz/pinout/automation_hat For infomation on the gpiozero library see here, https://gpiozero.readthedocs.io/en/stable/ + + This is a class object that represents the observatory dome. It tracks the + Dome azimuth using an encoder attached to the dome motor and a home sensor + that provides a reference azimuth position. + + The Dome class is initialised and run on a system consisting of a Raspberry + Pi and a Pimoroni AutomationHAT. If an AutomationHAT is not available, the + code can be run on an ordinary system in testing mode with the debug_lights + option disabled. """ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): - """Initialize raspberry pi GPIO environment.""" + """ + Initialize raspberry pi GPIO environment. + + If we require the code to run in testing mode we create some mock + hardware using the GPIOzero library. Otherwise the GPIOzero library + will automatically detect the GPIO pins of the Rasberry Pi. + + We then need to identify which pin numbers map to the devices on the + automationHAT that we want to make use of. See here for reference, + https://pinout.xyz/pinout/automation_hat + + Once the necessary pin numbers have been identified we can set about + creating GPIOzero objects to make use of the automationHat. This + includes, + - A digital input device for the encoder + - A digital input device for the home sensor + - A digital output device for the motor on/off relay switch + - A digital output device for the motor direction (CW/CCW) relay switch + + The digital input devices (DIDs) have callback functions that can be + used to call a function upon activation or deactivation. For example, + activating the encoder DID can be set to increment a Dome encoder + count instance variable. + + As part of initialisation several instance variables will be set to + designate status infomation about the dome such as dome azimuth + (unknown at initialisation) and position of the direction relay switch + (initialised in the CCW position). + """ if testing: # Set the default pin factory to a mock factory Device.pin_factory = MockFactory() @@ -71,9 +109,12 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): DIRECTION_RELAY_PIN_NUMBER = 19 # set a timeout length in seconds for wait_for_active() calls WAIT_TIMEOUT = 1 - # set a variable for bounce_time in seconds + # set a variable for bounce_time in seconds, this is just cool + # off period where the object will ignore additional (de)activation BOUNCE_TIME = 0.1 + # in testing mode we need to create a seperate pin object so we can + # simulate the activation of our fake DIDs and DODs self.encoder_pin = Device.pin_factory.pin(ENCODER_PIN_NUMBER) self.home_sensor_pin = Device.pin_factory.pin( HOME_SENSOR_PIN_NUMBER) @@ -90,16 +131,23 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): DIRECTION_RELAY_PIN_NUMBER = 19 # set the timeout length variable to None for non testing mode WAIT_TIMEOUT = None - # set a variable for bounce_time in seconds + # set a variable for bounce_time in seconds, this is just cool + # off period where the object will ignore additional (de)activation BOUNCE_TIME = 0.1 if debug_lights: - # led_status is binary number, each zero/position sets the state - # of an LED, where 0 is off and 1 is on + # led_status is set with binary number, each zero/position sets the + # state of an LED, where 0 is off and 1 is on self.led_status = 0b000000000000000000 sn3218.output([0x10] * 18) sn3218.enable_leds(self.led_status) sn3218.enable() + # create a dictionary of the LED name as the key and the sigit of + # the self.led_status binary int it corresponds to. This means we + # can convert the binary int to a string and use the index to + # change a 0 to 1 and vice versa and then convert back to a binary + # int. The updated self.led_status can then be sent to the LED + # controller. self.led_lights_ind = { 'power': 2, 'comms': 3, @@ -125,7 +173,7 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): # calibrated az self.testing = testing self.debug_lights = debug_lights - self.dome_status = "unknown" + self._dome_status = "unknown" self.dome_az = None # create a instance variable to track the dome motor encoder ticks @@ -159,6 +207,8 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): DIRECTION_RELAY_PIN_NUMBER, initial_value=False) # because we initiliase the relay in the nc position self.current_direction = "CCW" + + # turn on the relay LEDs if we are debugging if debug_lights: self._turn_led_on(leds=['relay_2_nc']) self._turn_led_on(leds=['relay_1_nc']) @@ -180,7 +230,7 @@ def is_home(self): @property def status(self): """Return a text string describing dome rotators current status.""" - pass + return self._dome_status ############################################################################### # Methods @@ -189,18 +239,21 @@ def status(self): """These map directly onto the AbstractMethods created by RPC.""" def abort(self): - """Stop everything.""" + """Stop everything by switching the dome motor on/off relay to off.""" + # note, might want another way to do this incase the relay fails/sticks + # 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? self._stop_moving() pass def getAz(self): - """Return AZ.""" + """Return current Azimuth of the Dome.""" if self.dome_az is None: self.calibrate() return self.dome_az def GotoAz(self, az): - "Send Dome to Az." + "Send Dome to a requested Azimuth position." if self.dome_az is None: self.calibrate() delta_az = az - self.dome_az @@ -269,6 +322,7 @@ def calibrate(self, num_cal_rotations=2): self._move_cw() self.home_sensor.wait_for_active(timeout=self.wait_timeout) if self.testing: + # in testing mode we need to "fake" the activation of the home pin self.home_sensor_pin.drive_high() time.sleep(0.1) self._stop_moving() @@ -281,16 +335,20 @@ def calibrate(self, num_cal_rotations=2): time.sleep(0.5) self._move_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) 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 self.home_sensor_pin.drive_high() time.sleep(0.5) self._stop_moving() rotation_count += 1 + # set the azimuth per encoder tick factor based on how many ticks we + # counted over n rotations self.az_per_tick = 360 / (self.encoder_count / rotation_count) pass @@ -299,15 +357,26 @@ def calibrate(self, num_cal_rotations=2): ############################################################################### def _set_at_home(self): + """Update home status to at home and debug LEDs (if enabled).""" self._turn_led_on(leds=['input_2']) self._at_home = True def _set_not_home(self): + """Update home status to not at home and debug LEDs (if enabled).""" self._turn_led_off(leds=['input_2']) self._at_home = False def _increment_count(self): - # Unsure what purpose the device variable is supposed to serve here? + """ + Private method used for callback function of the encoder DOD. + + Calling this method will toggle the encoder debug LED (if enabled) + and increment or decrement the encoder_count instance variable, + depending on the current rotation direction of the dome. + + If the current dome direction cannot be determined, the last recorded + direction is adopted. + """ print(f"Encoder activated _increment_count") self._turn_led_on(leds=['input_1']) time.sleep(0.01) @@ -316,6 +385,7 @@ def _increment_count(self): self.encoder_count += 1 elif self.current_direction == "CCW": self.encoder_count -= 1 + # I'm unsure if this is the best way to handle a situation like this elif self.current_direction is None: if self.last_direction == "CW": self.encoder_count += 1 @@ -323,66 +393,92 @@ def _increment_count(self): self.encoder_count -= 1 def _az_to_ticks(self, az): + """Convert degrees (azimuth) to equivalent in encoder tick count.""" return az / self.az_per_tick def _ticks_to_az(self, ticks): + """Convert encoder tick count to equivalent in degrees (azimuth).""" return ticks * self.az_per_tick def _move_cw(self): + """Set dome to move clockwise.""" + # if testing, deactivate the home_sernsor_pin to simulate leaving home if self.testing and self._at_home: self.home_sensor_pin.drive_low() + # update the last_direction instance variable self.last_direction = self.current_direction + # now update the current_direction variable to CW self.current_direction = "CW" # set the direction relay switch to CW position self.direction_relay.on() + # update the debug LEDs (will only do something if they are enabled) if self.last_direction == "CCW": self._turn_led_off(leds=['relay_2_nc']) self._turn_led_on(leds=['relay_2_no']) # turn on rotation self.rotation_relay.on() + # update the rotation relay debug LEDs self._turn_led_on(leds=['relay_1_no']) self._turn_led_off(leds=['relay_1_nc']) cmd_status = True return cmd_status def _move_ccw(self): + """Set dome to move counter-clockwise.""" + # if testing, deactivate the home_sernsor_pin to simulate leaving home if self.testing and self._at_home: self.home_sensor_pin.drive_low() + # update the last_direction instance variable self.last_direction = self.current_direction + # now update the current_direction variable to CCW self.current_direction = "CCW" # set the direction relay switch to CCW position self.direction_relay.off() + # update the debug LEDs (will only do something if they are enabled) if self.last_direction == "CW": self._turn_led_off(leds=['relay_2_no']) self._turn_led_on(leds=['relay_2_nc']) # turn on rotation self.rotation_relay.on() + # update the rotation relay debug LEDs self._turn_led_on(leds=['relay_1_no']) self._turn_led_off(leds=['relay_1_nc']) cmd_status = True return cmd_status def _stop_moving(self): + """Stop dome movement by switching the dome rotation relay off.""" self.rotation_relay.off() + # update the debug LEDs self._turn_led_off(leds=['relay_1_no']) self._turn_led_on(leds=['relay_1_nc']) + # update last_direction with current_direction at time of method call self.last_direction = self.current_direction def _simulate_ticks(self, num_ticks): + """Method to simulate encoder ticks while in testing mode.""" tick_count = 0 + # repeat this loop of driving the mock pins low then high to simulate + # an encoder tick. Continue until desired number of ticks is reached. while tick_count < num_ticks: self.encoder_pin.drive_low() + # t_wait is set so that it will always exceed the set bounce_time + # of the pins time.sleep(self.t_wait) self.encoder_pin.drive_high() time.sleep(self.t_wait) tick_count += 1 def _turn_led_on(self, leds=[]): + """Method of turning a set of debugging LEDs on""" # pass a list of strings of the leds to turn on if self.debug_lights: + if leds == []: + # if leds is an empty list do nothing + pass # this function needs a bunch of checks at some point # like length of the binary number, whether things have the right - # type at the end etc etc + # type at the end (binary int vs string vs list) etc etc # # take the current led_status and convert to a string in binary # format (18bit) @@ -396,12 +492,17 @@ def _turn_led_on(self, leds=[]): # convert the updated list to a string and then to a binary int new_state = ''.join(new_state) self.led_status = int(new_state, 2) + # pass the new binary int to LED controller sn3218.enable_leds(self.led_status) pass def _turn_led_off(self, leds=[]): + """Method of turning a set of debugging LEDs off""" # pass a list of strings of the leds to turn off if self.debug_lights: + if leds == []: + # if leds is an empty list do nothing + pass # take the current led_status and convert to a string in binary # format (18bit) new_state = format(self.led_status, '#020b') @@ -414,5 +515,6 @@ def _turn_led_off(self, leds=[]): # convert the updated list to a string and then to a binary int new_state = ''.join(new_state) self.led_status = int(new_state, 2) + # pass the new binary int to LED controller sn3218.enable_leds(self.led_status) pass diff --git a/examples/dome_control_example.ipynb b/examples/dome_control_example.ipynb new file mode 100644 index 0000000..c64d6e5 --- /dev/null +++ b/examples/dome_control_example.ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from domehunter import Dome\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." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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", + "# 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)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n" + ] + } + ], + "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", + "testdome.calibrate()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dome rotates 1 degrees per encoder tick. Current encoder count is 0\n" + ] + } + ], + "source": [ + "# We can now check the the degrees per tick factor and the encoder count\n", + "print(f'Dome rotates {testdome.az_per_tick} degrees per encoder tick. Current encoder count is {testdome.encoder_count}.')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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", + "testdome.dome_az = 90\n", + "testdome.encoder_count = 9\n", + "testdome.az_per_tick = 10" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "90" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# check where the dome thinks it is\n", + "testdome.getAz()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n" + ] + } + ], + "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)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dome is currently at an azimuth of 290.0, with an encoder count of 29.0\n" + ] + } + ], + "source": [ + "# we can now check if the dome ended up where we wanted.\n", + "print(f'Dome is currently at an azimuth of {testdome.getAz()}, with an encoder count of {testdome.encoder_count}')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "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)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n", + "Encoder activated _increment_count\n" + ] + } + ], + "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)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dome is currently at an azimuth of 10.0, with an encoder count of 1.0\n" + ] + } + ], + "source": [ + "# we can now check if the dome ended up where we wanted.\n", + "print(f'Dome is currently at an azimuth of {testdome.getAz()}, with an encoder count of {testdome.encoder_count}')" + ] + }, + { + "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.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements.txt b/requirements.txt index 1346d46..273e613 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ astropy_helpers gpiozero +smbus +sn3218 From b57e5a31de9827737b64363e0bcf570b9ec9c3c1 Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 30 Jul 2019 15:34:01 +1000 Subject: [PATCH 08/18] formatting error --- README.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7d83b15..72e0703 100644 --- a/README.rst +++ b/README.rst @@ -100,11 +100,16 @@ Python RaspberryPi Component Requirements --------------- Required: -- `gpiozero` python library + +* `gpiozero` python library + Optional: -- `smbus` and `sn3218` python libraries -Note: The `smbus` and `sn3218` are used to control the automationHAT status +* `smbus` and `sn3218` python libraries + +Note: + +The `smbus` and `sn3218` are used to control the automationHAT status LEDs. If you plan on running the code without the automationHAT these libraries aren't required. From 0d86e2e17c8964bda78b72d30b1f155de8abaf85 Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 30 Jul 2019 15:35:21 +1000 Subject: [PATCH 09/18] formatting error2 --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 72e0703..3ca8eb2 100644 --- a/README.rst +++ b/README.rst @@ -101,22 +101,22 @@ Requirements --------------- Required: -* `gpiozero` python library +* ``gpiozero`` python library Optional: -* `smbus` and `sn3218` python libraries +* ``smbus`` and ``sn3218`` python libraries Note: -The `smbus` and `sn3218` are used to control the automationHAT status +The ``smbus`` and ``sn3218`` are used to control the automationHAT status LEDs. If you plan on running the code without the automationHAT these libraries aren't required. Getting Started --------------- Follow the example jupyter notebook in the examples direction -(`dome_control_example`). The automationHAT hardware is not required to run the +(``dome_control_example``). The automationHAT hardware is not required to run the code in testing mode. From 4029c2d0d14b4e48c880b7c8b965f1ae7ebead64 Mon Sep 17 00:00:00 2001 From: Lee Date: Wed, 31 Jul 2019 08:36:36 +1000 Subject: [PATCH 10/18] osx install, restructure --- README.rst | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 3ca8eb2..b64b791 100644 --- a/README.rst +++ b/README.rst @@ -33,27 +33,21 @@ Requirements --------------- ``grpc python`` See instructions `here `_. -``grpc c++`` See instructions `here `_. -Getting Started ---------------- -The code for the Huntsman dome driver is contained in the -``domehunter/protos/src`` directory. This directory contains both -human written files and files automatically generated by gRPC -tools. The human written files are, +``grpc c++`` See instructions `here `_. -* ``main.cpp`` -* ``main.h`` -* ``x2dome.cpp`` -* ``x2dome.h`` -* ``hx2dome.proto`` -* ``hx2dome.proto_server.py`` +To install the above on OSX, run: +`` +pip install --upgrade pip +pip install grpcio +brew install protobuf-c +brew install grpc -The remaining cpp and python files are automatically produced -by gRPC and shouldn't need to be looked at. If for some reason -you want to generate these files yourself, see the -*gRPC automatically generated files* section below. +`` + +Getting Started +--------------- The files for compilation and installation are found in the ``domehunter/protos/`` directory. The relevant files are, @@ -93,6 +87,23 @@ adjusted to your local machine. You shouldn't need to worry about this as the generated files are committed to the repositry and shouldn't need to be generated. +The code for the Huntsman dome driver is contained in the +``domehunter/protos/src`` directory. This directory contains both +human written files and files automatically generated by gRPC +tools. The human written files are, + +* ``main.cpp`` +* ``main.h`` +* ``x2dome.cpp`` +* ``x2dome.h`` +* ``hx2dome.proto`` +* ``hx2dome.proto_server.py`` + +The remaining cpp and python files are automatically produced +by gRPC and shouldn't need to be looked at. If for some reason +you want to generate these files yourself, see the +*gRPC automatically generated files* section below. + Python RaspberryPi Component ============================ From a7b752ddc11a033a0dc923a20bf39399d6316038 Mon Sep 17 00:00:00 2001 From: Lee Date: Wed, 31 Jul 2019 08:37:54 +1000 Subject: [PATCH 11/18] turn off all compilation warnings --- domehunter/protos/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domehunter/protos/Makefile b/domehunter/protos/Makefile index 9ef03f2..4f729bb 100644 --- a/domehunter/protos/Makefile +++ b/domehunter/protos/Makefile @@ -1,10 +1,10 @@ # Makefile for libHuntsmanDome CXX = g++ -CFLAGS = -fPIC -Wall -Wextra -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/\ +CFLAGS = -fPIC -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/\ -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread -I/usr/local/include -CPPFLAGS = -fPIC -Wall -Wextra -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/ -I/usr/local/include\ +CPPFLAGS = -fPIC -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/ -I/usr/local/include\ -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread CXXFLAGS += -std=c++11 From fdb0363abb3a78226d79b2c34bd1112d38308111 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 31 Jul 2019 14:19:17 +1000 Subject: [PATCH 12/18] fix small formatting error --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b64b791..ff4ba49 100644 --- a/README.rst +++ b/README.rst @@ -38,12 +38,12 @@ Requirements ``grpc c++`` See instructions `here `_. To install the above on OSX, run: + `` pip install --upgrade pip pip install grpcio brew install protobuf-c brew install grpc - `` Getting Started From 372c580eaa319628d1a9508f6434ba525bdabd44 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 31 Jul 2019 14:22:21 +1000 Subject: [PATCH 13/18] fix2 fix2 --- README.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index ff4ba49..e5b2376 100644 --- a/README.rst +++ b/README.rst @@ -37,14 +37,13 @@ Requirements ``grpc c++`` See instructions `here `_. -To install the above on OSX, run: - -`` -pip install --upgrade pip -pip install grpcio -brew install protobuf-c -brew install grpc -`` +To install the above on OSX, run:: + + pip install --upgrade pip + pip install grpcio + brew install protobuf-c + brew install grpc + Getting Started --------------- From 68eea594b6e36d85bb0c1d697a6c3eb5f82c72bb Mon Sep 17 00:00:00 2001 From: fergus Date: Fri, 16 Aug 2019 03:35:21 +1000 Subject: [PATCH 14/18] tidy up --- domehunter/__init__.py | 389 +++++++++++++++++++++++++---------------- requirements.txt | 4 +- 2 files changed, 239 insertions(+), 154 deletions(-) diff --git a/domehunter/__init__.py b/domehunter/__init__.py index 22e4515..fccf971 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -18,11 +18,9 @@ except OSError: warnings.warn( "AutomationHAT hardware not detected, testing=True and debug_lights=False recommended.") - pass except: warnings.warn( "Something went wrong in importing sn3218, status lights unlikely to work.") - pass # ---------------------------------------------------------------------------- @@ -50,11 +48,6 @@ class Dome(): """ Interface to dome control raspberry pi GPIO. - To see the gpio pin mapping to the automation HAT see here, - https://pinout.xyz/pinout/automation_hat - For infomation on the gpiozero library see here, - https://gpiozero.readthedocs.io/en/stable/ - This is a class object that represents the observatory dome. It tracks the Dome azimuth using an encoder attached to the dome motor and a home sensor that provides a reference azimuth position. @@ -63,55 +56,67 @@ class Dome(): Pi and a Pimoroni AutomationHAT. If an AutomationHAT is not available, the code can be run on an ordinary system in testing mode with the debug_lights option disabled. + + To see the gpio pin mapping to the automation HAT see here, + https://pinout.xyz/pinout/automation_hat + For infomation on the gpiozero library see here, + https://gpiozero.readthedocs.io/en/stable/ """ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): """ Initialize raspberry pi GPIO environment. - If we require the code to run in testing mode we create some mock + Default settings are to run in testing mode, where we create some mock hardware using the GPIOzero library. Otherwise the GPIOzero library - will automatically detect the GPIO pins of the Rasberry Pi. + will automatically detect the GPIO pins of the Rasberry Pi when + 'testing=False'. With 'debug_lights=True', LEDs on AutomationHat will + be used to indicate current state. - We then need to identify which pin numbers map to the devices on the - automationHAT that we want to make use of. See here for reference, + Next the GIPO pins must be mapped to the correct sensor (e.g. home + sensor) they are attached to on the automationHAT that we want to make + use of. See here for reference, https://pinout.xyz/pinout/automation_hat Once the necessary pin numbers have been identified we can set about creating GPIOzero objects to make use of the automationHat. This includes, - - A digital input device for the encoder - - A digital input device for the home sensor + - A digital input device for the encoder (`ENCODER_PIN_NUMBER`) + - A digital input device for the home sensor (`HOME_SENSOR_PIN_NUMBER`) - A digital output device for the motor on/off relay switch + (`ROTATION_RELAY_PIN_NUMBER`) - A digital output device for the motor direction (CW/CCW) relay switch + (`DIRECTION_RELAY_PIN_NUMBER`) - The digital input devices (DIDs) have callback functions that can be - used to call a function upon activation or deactivation. For example, - activating the encoder DID can be set to increment a Dome encoder - count instance variable. + The Digital Input Devices (DIDs) have callback functions that can be + used to call a function upon activation or deactivation of a sensor + via a GPIO. For example, activating the encoder DID can be set to + increment a Dome encoder count instance variable. As part of initialisation several instance variables will be set to designate status infomation about the dome such as dome azimuth (unknown at initialisation) and position of the direction relay switch (initialised in the CCW position). """ + + # input 1 on automation hat + ENCODER_PIN_NUMBER = 26 + # input 2 on the automation hat + HOME_SENSOR_PIN_NUMBER = 20 + # relay 1 on automation hat + ROTATION_RELAY_PIN_NUMBER = 13 + # relay 2 on automation hat + # on position for CW and off for CCW + DIRECTION_RELAY_PIN_NUMBER = 19 + # set a timeout length in seconds for wait_for_active() calls + WAIT_TIMEOUT = 1 + # set a variable for bounce_time in seconds, this is just cool + # off period where the object will ignore additional (de)activation + BOUNCE_TIME = 0.1 + if testing: # Set the default pin factory to a mock factory Device.pin_factory = MockFactory() - # input 1 on automation hat - ENCODER_PIN_NUMBER = 26 - # input 2 on the automation hat - HOME_SENSOR_PIN_NUMBER = 20 - # relay 1 on automation hat - ROTATION_RELAY_PIN_NUMBER = 13 - # relay 2 on automation hat - # on position for CW and off for CCW? - DIRECTION_RELAY_PIN_NUMBER = 19 - # set a timeout length in seconds for wait_for_active() calls - WAIT_TIMEOUT = 1 - # set a variable for bounce_time in seconds, this is just cool - # off period where the object will ignore additional (de)activation - BOUNCE_TIME = 0.1 # in testing mode we need to create a seperate pin object so we can # simulate the activation of our fake DIDs and DODs @@ -119,21 +124,13 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): self.home_sensor_pin = Device.pin_factory.pin( HOME_SENSOR_PIN_NUMBER) else: - """ Do not change until you're sure!!! """ - # input 1 on automation hat - ENCODER_PIN_NUMBER = 26 - # input 2 on the automation hat - HOME_SENSOR_PIN_NUMBER = 20 - # relay 1 on automation hat - ROTATION_RELAY_PIN_NUMBER = 13 - # relay 2 on automation hat - # no position for CW and nc for CCW? - DIRECTION_RELAY_PIN_NUMBER = 19 # set the timeout length variable to None for non testing mode WAIT_TIMEOUT = None - # set a variable for bounce_time in seconds, this is just cool - # off period where the object will ignore additional (de)activation - BOUNCE_TIME = 0.1 + + # set a wait time for testing mode that exceeds BOUNCE_TIME + self.test_mode_delay_duration = BOUNCE_TIME + 0.05 + # set the timeout for wait_for_active() + self.wait_timeout = WAIT_TIMEOUT if debug_lights: # led_status is set with binary number, each zero/position sets the @@ -142,12 +139,12 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): sn3218.output([0x10] * 18) sn3218.enable_leds(self.led_status) sn3218.enable() - # create a dictionary of the LED name as the key and the sigit of - # the self.led_status binary int it corresponds to. This means we - # can convert the binary int to a string and use the index to - # change a 0 to 1 and vice versa and then convert back to a binary - # int. The updated self.led_status can then be sent to the LED - # controller. + # create a dictionary of the LED name as the key and the digit of + # the self.led_status binary integer it corresponds to. This means + # we can convert the binary integer to a string and use the index + # to change a 0 to 1 and vice versa and then convert back to a + # binary integer. The updated self.led_status can then be sent to + # the LED controller. self.led_lights_ind = { 'power': 2, 'comms': 3, @@ -155,12 +152,12 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): 'input_1': 5, 'input_2': 6, 'input_3': 7, - 'relay_3_nc': 8, - 'relay_3_no': 9, - 'relay_2_nc': 10, - 'relay_2_no': 11, - 'relay_1_nc': 12, - 'relay_1_no': 13, + 'relay_3_normally_closed': 8, + 'relay_3_normally_open': 9, + 'relay_2_normally_closed': 10, + 'relay_2_normally_open': 11, + 'relay_1_normally_closed': 12, + 'relay_1_normally_open': 13, 'output_3': 14, 'output_2': 15, 'output_1': 16, @@ -184,10 +181,10 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): ENCODER_PIN_NUMBER, bounce_time=BOUNCE_TIME) # _increment_count function to run when encoder is triggered self.encoder.when_activated = self._increment_count - # dummy value - self.az_per_tick = 1 + # set dummy value initially to force a rotation calibration run + self.az_per_tick = None - self._at_home = False + self._set_not_home() self.home_sensor = DigitalInputDevice( HOME_SENSOR_PIN_NUMBER, bounce_time=BOUNCE_TIME) # _set_not_home function is run when upon home senser deactivation @@ -198,25 +195,20 @@ def __init__(self, testing=True, debug_lights=False, *args, **kwargs): # these two DODs control the relays that control the dome motor # the rotation relay is the on/off switch for dome rotation # the direction relay will toggle either the CW or CCW direction - # (using both the normally open and normally close relay terminals)\ + # (using both the normally open and normally close relay terminals) # so when moving the dome, first set the direction relay position # then activate the rotation relay self.rotation_relay = DigitalOutputDevice( ROTATION_RELAY_PIN_NUMBER, initial_value=False) - self.direction_relay = DigitalOutputDevice( + self.direction_CW_relay = DigitalOutputDevice( DIRECTION_RELAY_PIN_NUMBER, initial_value=False) - # because we initiliase the relay in the nc position + # because we initialiase the relay in the nnormally closed position self.current_direction = "CCW" # turn on the relay LEDs if we are debugging if debug_lights: - self._turn_led_on(leds=['relay_2_nc']) - self._turn_led_on(leds=['relay_1_nc']) - - # set a wait time for testing mode that exceeds BOUNCE_TIME - self.t_wait = BOUNCE_TIME + 0.05 - # set the timeout for wait_for_active() - self.wait_timeout = WAIT_TIMEOUT + self._turn_led_on(leds=['relay_2_normally_closed']) + self._turn_led_on(leds=['relay_1_normally_closed']) ############################################################################### # Properties @@ -239,23 +231,44 @@ def status(self): """These map directly onto the AbstractMethods created by RPC.""" def abort(self): - """Stop everything by switching the dome motor on/off relay to off.""" - # note, might want another way to do this incase the relay fails/sticks + """ + Stop everything by switching the dome motor on/off relay to off. + + """ + # TODO: consider another way to do this in case the relay fails/sticks # 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? self._stop_moving() - pass def getAz(self): - """Return current Azimuth of the Dome.""" + """ + Return current Azimuth of the Dome. + + Returns + ------- + float + The Dome azimuth in degrees. + + """ if self.dome_az is None: - self.calibrate() + print("Cannot return Azimuth as Dome is not yet calibrated.\ + Run calibration loop") return self.dome_az def GotoAz(self, az): - "Send Dome to a requested Azimuth position." + """ + Send Dome to a requested Azimuth position. + + Parameters + ---------- + az : float + Desired dome azimuth position in degrees. + + """ if self.dome_az is None: - self.calibrate() + print('Dome is not yet calibrated, running through calibration\ + procedure, then will go to AZ specified.') + self.calibrate_dome_encoder_counts() delta_az = az - self.dome_az # determine whether CW or CCW gives the short path to desired az @@ -276,7 +289,6 @@ def GotoAz(self, az): if self.testing: # if testing simulate a tick for every cycle of while loop self._simulate_ticks(num_ticks=1) - pass self._stop_moving() # compare original count to current just in case we got more ticks # than we asked for @@ -288,7 +300,6 @@ def GotoAz(self, az): self.dome_az %= 360 # update encoder_count to match the dome_az self.encoder_count = self._az_to_ticks(self.dome_az) - pass # if updated delta_az is negative, direction is CCW if delta_az < 0: @@ -301,7 +312,9 @@ def GotoAz(self, az): if self.testing: # if testing simulate a tick for every cycle of while loop self._simulate_ticks(num_ticks=1) - pass + else: + # micro break to spare the little rpi cpu + time.sleep(0.1) self._stop_moving() # compare original count to current, just in case we got more ticks # than we asked for @@ -313,11 +326,17 @@ def GotoAz(self, az): self.dome_az %= 360 # update encoder_count to match the dome_az self.encoder_count = self._az_to_ticks(self.dome_az) - pass - pass - def calibrate(self, num_cal_rotations=2): - """Calibrate the encoder (determine degrees per tick)""" + def calibrate_dome_encoder_counts(self, num_cal_rotations=2): + """ + Calibrate the encoder (determine degrees per tick). + + Parameters + ---------- + num_cal_rotations : integer + Number of rotations to perform to calibrate encoder. + + """ # rotate the dome until we hit home, to give reference point self._move_cw() self.home_sensor.wait_for_active(timeout=self.wait_timeout) @@ -350,19 +369,22 @@ def calibrate(self, num_cal_rotations=2): # set the azimuth per encoder tick factor based on how many ticks we # counted over n rotations self.az_per_tick = 360 / (self.encoder_count / rotation_count) - pass ############################################################################### # Private Methods ############################################################################### def _set_at_home(self): - """Update home status to at home and debug LEDs (if enabled).""" + """ + Update home status to at home and debug LEDs (if enabled). + """ self._turn_led_on(leds=['input_2']) self._at_home = True def _set_not_home(self): - """Update home status to not at home and debug LEDs (if enabled).""" + """ + Update home status to not at home and debug LEDs (if enabled). + """ self._turn_led_off(leds=['input_2']) self._at_home = False @@ -393,15 +415,49 @@ def _increment_count(self): self.encoder_count -= 1 def _az_to_ticks(self, az): - """Convert degrees (azimuth) to equivalent in encoder tick count.""" + """ + Convert degrees (azimuth) to equivalent in encoder tick count. + + Parameters + ---------- + az : float + Dome azimuth position in degrees. + + Returns + ------- + float + Returns azimuth position to corresponding encoder tick count. + + """ return az / self.az_per_tick def _ticks_to_az(self, ticks): - """Convert encoder tick count to equivalent in degrees (azimuth).""" + """ + Convert encoder tick count to equivalent in degrees (azimuth). + + Parameters + ---------- + ticks : integer + The number of encoder ticks recorded. + + Returns + ------- + float + The corresponding dome azimuth position in degrees. + + """ return ticks * self.az_per_tick def _move_cw(self): - """Set dome to move clockwise.""" + """ + Set dome to move clockwise. + + Returns + ------- + integer + Command status return code (tbd). + + """ # if testing, deactivate the home_sernsor_pin to simulate leaving home if self.testing and self._at_home: self.home_sensor_pin.drive_low() @@ -410,21 +466,29 @@ def _move_cw(self): # now update the current_direction variable to CW self.current_direction = "CW" # set the direction relay switch to CW position - self.direction_relay.on() - # update the debug LEDs (will only do something if they are enabled) + self.direction_CW_relay.on() + # update the debug LEDs if self.last_direction == "CCW": - self._turn_led_off(leds=['relay_2_nc']) - self._turn_led_on(leds=['relay_2_no']) + self._turn_led_off(leds=['relay_2_normally_closed']) + self._turn_led_on(leds=['relay_2_normally_open']) # turn on rotation self.rotation_relay.on() # update the rotation relay debug LEDs - self._turn_led_on(leds=['relay_1_no']) - self._turn_led_off(leds=['relay_1_nc']) + self._turn_led_on(leds=['relay_1_normally_open']) + self._turn_led_off(leds=['relay_1_normally_closed']) cmd_status = True return cmd_status def _move_ccw(self): - """Set dome to move counter-clockwise.""" + """ + Set dome to move counter-clockwise. + + Returns + ------- + integer + Command status return code (tbd). + + """ # if testing, deactivate the home_sernsor_pin to simulate leaving home if self.testing and self._at_home: self.home_sensor_pin.drive_low() @@ -433,88 +497,109 @@ def _move_ccw(self): # now update the current_direction variable to CCW self.current_direction = "CCW" # set the direction relay switch to CCW position - self.direction_relay.off() - # update the debug LEDs (will only do something if they are enabled) + self.direction_CW_relay.off() + # update the debug LEDs if self.last_direction == "CW": - self._turn_led_off(leds=['relay_2_no']) - self._turn_led_on(leds=['relay_2_nc']) + self._turn_led_off(leds=['relay_2_normally_open']) + self._turn_led_on(leds=['relay_2_normally_closed']) # turn on rotation self.rotation_relay.on() # update the rotation relay debug LEDs - self._turn_led_on(leds=['relay_1_no']) - self._turn_led_off(leds=['relay_1_nc']) + self._turn_led_on(leds=['relay_1_normally_open']) + self._turn_led_off(leds=['relay_1_normally_closed']) cmd_status = True return cmd_status def _stop_moving(self): - """Stop dome movement by switching the dome rotation relay off.""" + """ + Stop dome movement by switching the dome rotation relay off. + """ self.rotation_relay.off() # update the debug LEDs - self._turn_led_off(leds=['relay_1_no']) - self._turn_led_on(leds=['relay_1_nc']) + self._turn_led_off(leds=['relay_1_normally_open']) + self._turn_led_on(leds=['relay_1_normally_closed']) # update last_direction with current_direction at time of method call self.last_direction = self.current_direction def _simulate_ticks(self, num_ticks): - """Method to simulate encoder ticks while in testing mode.""" + """ + Method to simulate encoder ticks while in testing mode. + """ tick_count = 0 # repeat this loop of driving the mock pins low then high to simulate # an encoder tick. Continue until desired number of ticks is reached. while tick_count < num_ticks: self.encoder_pin.drive_low() - # t_wait is set so that it will always exceed the set bounce_time + # test_mode_delay_duration is set so that it will always exceed + # the set bounce_time # of the pins - time.sleep(self.t_wait) + time.sleep(self.test_mode_delay_duration) self.encoder_pin.drive_high() - time.sleep(self.t_wait) + time.sleep(self.test_mode_delay_duration) tick_count += 1 def _turn_led_on(self, leds=[]): - """Method of turning a set of debugging LEDs on""" + """ + Method of turning a set of debugging LEDs on + + Parameters + ---------- + leds : list + List of LED name string to indicate which LEDs to turn on. + + """ # pass a list of strings of the leds to turn on - if self.debug_lights: - if leds == []: - # if leds is an empty list do nothing - pass - # this function needs a bunch of checks at some point - # like length of the binary number, whether things have the right - # type at the end (binary int vs string vs list) etc etc - # - # take the current led_status and convert to a string in binary - # format (18bit) - new_state = format(self.led_status, '#020b') - # from that string create a list of characters - # use the keys in the leds list and the led_lights_ind - new_state = list(new_state) - for led in leds: - ind = self.led_lights_ind[led] - new_state[ind] = '1' - # convert the updated list to a string and then to a binary int - new_state = ''.join(new_state) - self.led_status = int(new_state, 2) - # pass the new binary int to LED controller - sn3218.enable_leds(self.led_status) - pass + if not(self.debug_lights): + return None + if leds == []: + # if leds is an empty list do nothing + pass + # this function needs a bunch of checks at some point + # like length of the binary number, whether things have the right + # type at the end (binary int vs string vs list) etc etc + # + # take the current led_status and convert to a string in binary + # format (18bit) + new_state = format(self.led_status, '#020b') + # from that string create a list of characters + # use the keys in the leds list and the led_lights_ind + new_state = list(new_state) + for led in leds: + ind = self.led_lights_ind[led] + new_state[ind] = '1' + # convert the updated list to a string and then to a binary int + new_state = ''.join(new_state) + self.led_status = int(new_state, 2) + # pass the new binary int to LED controller + sn3218.enable_leds(self.led_status) def _turn_led_off(self, leds=[]): - """Method of turning a set of debugging LEDs off""" + """ + Method of turning a set of debugging LEDs off. + + Parameters + ---------- + leds : list + List of LED name string to indicate which LEDs to turn off. + + """ # pass a list of strings of the leds to turn off if self.debug_lights: - if leds == []: - # if leds is an empty list do nothing - pass - # take the current led_status and convert to a string in binary - # format (18bit) - new_state = format(self.led_status, '#020b') - # from that string create a list of characters - # use the keys in the leds list and the led_lights_ind - new_state = list(new_state) - for led in leds: - ind = self.led_lights_ind[led] - new_state[ind] = '0' - # convert the updated list to a string and then to a binary int - new_state = ''.join(new_state) - self.led_status = int(new_state, 2) - # pass the new binary int to LED controller - sn3218.enable_leds(self.led_status) - pass + return None + if leds == []: + # if leds is an empty list do nothing + pass + # take the current led_status and convert to a string in binary + # format (18bit) + new_state = format(self.led_status, '#020b') + # from that string create a list of characters + # use the keys in the leds list and the led_lights_ind + new_state = list(new_state) + for led in leds: + ind = self.led_lights_ind[led] + new_state[ind] = '0' + # convert the updated list to a string and then to a binary int + new_state = ''.join(new_state) + self.led_status = int(new_state, 2) + # pass the new binary int to LED controller + sn3218.enable_leds(self.led_status) diff --git a/requirements.txt b/requirements.txt index 273e613..4716d34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ astropy_helpers gpiozero -smbus -sn3218 +smbus # optional for LED use +sn3218 # optional for LED use From c410ad6672f5299f1a6ad2a3e0133d97768537f2 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 16 Aug 2019 15:57:37 +1000 Subject: [PATCH 15/18] tweaks to make/install files --- domehunter/protos/Makefile | 4 ++-- domehunter/protos/TheSkyX_plugin_install.sh | 4 ++-- domehunter/protos/domelist HuntsmanDome.txt | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/domehunter/protos/Makefile b/domehunter/protos/Makefile index 4f729bb..e0f3ac5 100644 --- a/domehunter/protos/Makefile +++ b/domehunter/protos/Makefile @@ -5,12 +5,12 @@ CFLAGS = -fPIC -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/\ -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread -I/usr/local/include CPPFLAGS = -fPIC -O2 -g -DSB_LINUX_BUILD -I. -I./src/licensedinterfaces/ -I/usr/local/include\ - -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread + -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread CXXFLAGS += -std=c++11 LDFLAGS = -shared -lstdc++ -L/usr/local/lib -lprotobuf -pthread -lgrpc++\ - -Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed -ldl + -lgrpc++_reflection -ldl RM = rm -f diff --git a/domehunter/protos/TheSkyX_plugin_install.sh b/domehunter/protos/TheSkyX_plugin_install.sh index d9ee590..41b76f1 100755 --- a/domehunter/protos/TheSkyX_plugin_install.sh +++ b/domehunter/protos/TheSkyX_plugin_install.sh @@ -34,10 +34,10 @@ fi cp "./domelist HuntsmanDome.txt" "$TheSkyX_Path/Resources/Common/Miscellaneous Files/" cp "./libHuntsmanDome.so" "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/" -app_owner=`/usr/bin/stat -c "%u" "$TheSkyX_Path" | xargs id -n -u` +# NOTE if trying to install on a linux machine change "/usr/bin/stat -f " to "/usr/bin/stat -c " +app_owner=`/usr/bin/stat -f "%u" "$TheSkyX_Path" | xargs id -n -u` if [ ! -z "$app_owner" ]; then chown $app_owner "$TheSkyX_Path/Resources/Common/Miscellaneous Files/domelist HuntsmanDome.txt" chown $app_owner "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/libHuntsmanDome.so" fi chmod 755 "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/libHuntsmanDome.so" - diff --git a/domehunter/protos/domelist HuntsmanDome.txt b/domehunter/protos/domelist HuntsmanDome.txt index 9cdca48..2b11e99 100644 --- a/domehunter/protos/domelist HuntsmanDome.txt +++ b/domehunter/protos/domelist HuntsmanDome.txt @@ -1,4 +1,3 @@ //See hardwarelist.txt for details on this file format. //Version|Manufacturer|Model|Comment|MapsTo|PlugInDllName|X2Developer|Windows|Mac|Linux| -1|Huntsman Telescope|Huntsman Telescope Dome Controller v1| | |libHuntsmanDome||0|0|1| - +1|Huntsman Telescope|Huntsman Telescope Dome Controller v1| | |libHuntsmanDome||0|1|1| From 4081e4d4e5613bb55ebf64587bd05fb315aa6611 Mon Sep 17 00:00:00 2001 From: fergus Date: Tue, 20 Aug 2019 12:08:23 +1000 Subject: [PATCH 16/18] update --- README.rst | 6 ++- .../protos/{Makefile => Makefile_LINUX} | 0 domehunter/protos/Makefile_MAC | 43 +++++++++++++++++++ .../protos/TheSkyX_LINUX_plugin_install.sh | 43 +++++++++++++++++++ ...stall.sh => TheSkyX_MAC_plugin_install.sh} | 4 +- domehunter/protos/generate_grpc_cpp_code.sh | 9 ++-- .../protos/generate_grpc_python_code.sh | 7 ++- 7 files changed, 99 insertions(+), 13 deletions(-) rename domehunter/protos/{Makefile => Makefile_LINUX} (100%) create mode 100644 domehunter/protos/Makefile_MAC create mode 100755 domehunter/protos/TheSkyX_LINUX_plugin_install.sh rename domehunter/protos/{TheSkyX_plugin_install.sh => TheSkyX_MAC_plugin_install.sh} (93%) diff --git a/README.rst b/README.rst index 8f0d8b5..22a888c 100644 --- a/README.rst +++ b/README.rst @@ -41,8 +41,10 @@ To install the above on OSX, run:: pip install --upgrade pip pip install grpcio - brew install protobuf-c - brew install grpc + pip install grpcio-tools + brew tap grpc/grpc + brew install -s -- --with-plugins grpc + brew install protobuf Getting Started --------------- diff --git a/domehunter/protos/Makefile b/domehunter/protos/Makefile_LINUX similarity index 100% rename from domehunter/protos/Makefile rename to domehunter/protos/Makefile_LINUX diff --git a/domehunter/protos/Makefile_MAC b/domehunter/protos/Makefile_MAC new file mode 100644 index 0000000..f461116 --- /dev/null +++ b/domehunter/protos/Makefile_MAC @@ -0,0 +1,43 @@ +# Makefile for libHuntsmanDome + +CXX = /Library/Developer/CommandLineTools/usr/bin/clang++ + +CFLAGS = -pipe -O2 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ + -fPIC -DSB_MAC_BUILD -g -I. -I./src/licensedinterfaces\ + -pthread -I/usr/local/include + +CPPFLAGS = -pipe -O2 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ + -fPIC -DSB_MAC_BUILD -g -I. -I./src/licensedinterfaces\ + -pthread -I/usr/local/include + +CXXFLAGS += -pipe -stdlib=libc++ -O2 -std=gnu++11 -isysroot\ + /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -fPIC -DSB_MAC_BUILD + +LDFLAGS = -stdlib=libc++ -headerpad_max_install_names\ + -Wl,-syslibroot,/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ + -Wl,-rpath,@executable_path/Frameworks\ + -Wl,-rpath,/User/bob/anaconda3/envs/huntsman/lib\ + -single_module -dynamiclib -L/usr/local/lib -lprotobuf -pthread\ + -lgrpc++ -lgrpc++_reflection -ldl -lgrpc + +RM = rm -f +STRIP = strip +TARGET_LIB = libHuntsmanDome.so + +SRCS = src/main.cpp src/x2dome.cpp src/hx2dome.grpc.pb.cpp src/hx2dome.pb.cpp + +OBJS = $(SRCS:.cpp=.o) + +.PHONY: all +all: ${TARGET_LIB} + +$(TARGET_LIB): $(OBJS) + $(CXX) $^ ${LDFLAGS} -o $@ + $(STRIP) $@ >/dev/null 2>&1 || true + +$(SRCS:.cpp=.d):%.d:%.cpp + $(CXX) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -MM $< >$@ + +.PHONY: clean +clean: + ${RM} ${TARGET_LIB} ${OBJS} diff --git a/domehunter/protos/TheSkyX_LINUX_plugin_install.sh b/domehunter/protos/TheSkyX_LINUX_plugin_install.sh new file mode 100755 index 0000000..0c4d36c --- /dev/null +++ b/domehunter/protos/TheSkyX_LINUX_plugin_install.sh @@ -0,0 +1,43 @@ +#!/bin/bash + + +TheSkyX_Install=`/usr/bin/find ~/Library/Application\ Support/Software\ Bisque/ -name TheSkyXInstallPath.txt` +echo "TheSkyX_Install = $TheSkyX_Install" + +if [ ! -f "$TheSkyX_Install" ]; then + echo TheSkyXInstallPath.txt not found + TheSkyX_Path=`/usr/bin/find ~/ -maxdepth 3 -name TheSkyX` + if [ -d "$TheSkyX_Path" ]; then + TheSkyX_Path="${TheSkyX_Path}/Contents" + else + echo TheSkyX application was not found. + exit 1 + fi +else + TheSkyX_Path=$(<"$TheSkyX_Install") +fi + +echo "Installing to $TheSkyX_Path" + + +if [ ! -d "$TheSkyX_Path" ]; then + echo TheSkyX Install dir not exist + exit 1 +fi + +if [ -d "$TheSkyX_Path/Resources/Common/PlugIns64" ]; then + PLUGINS_DIR="PlugIns64" +else + PLUGINS_DIR="PlugIns" +fi + +cp "./domelist HuntsmanDome.txt" "$TheSkyX_Path/Resources/Common/Miscellaneous Files/" +cp "./libHuntsmanDome.so" "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/" + +# NOTE if trying to install on a mac machine change "/usr/bin/stat -c" to "usr/bin/stat -f" +app_owner=`/usr/bin/stat -c "%u" "$TheSkyX_Path" | xargs id -n -u` +if [ ! -z "$app_owner" ]; then + chown $app_owner "$TheSkyX_Path/Resources/Common/Miscellaneous Files/domelist HuntsmanDome.txt" + chown $app_owner "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/libHuntsmanDome.so" +fi +chmod 755 "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/libHuntsmanDome.so" diff --git a/domehunter/protos/TheSkyX_plugin_install.sh b/domehunter/protos/TheSkyX_MAC_plugin_install.sh similarity index 93% rename from domehunter/protos/TheSkyX_plugin_install.sh rename to domehunter/protos/TheSkyX_MAC_plugin_install.sh index 41b76f1..cf9b285 100755 --- a/domehunter/protos/TheSkyX_plugin_install.sh +++ b/domehunter/protos/TheSkyX_MAC_plugin_install.sh @@ -34,8 +34,8 @@ fi cp "./domelist HuntsmanDome.txt" "$TheSkyX_Path/Resources/Common/Miscellaneous Files/" cp "./libHuntsmanDome.so" "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/" -# NOTE if trying to install on a linux machine change "/usr/bin/stat -f " to "/usr/bin/stat -c " -app_owner=`/usr/bin/stat -f "%u" "$TheSkyX_Path" | xargs id -n -u` +# NOTE if trying to install on a linux machine change "/usr/bin/stat -f" to "usr/bin/stat -c" +app_owner=`/usr/bin/stat -c "%u" "$TheSkyX_Path" | xargs id -n -u` if [ ! -z "$app_owner" ]; then chown $app_owner "$TheSkyX_Path/Resources/Common/Miscellaneous Files/domelist HuntsmanDome.txt" chown $app_owner "$TheSkyX_Path/Resources/Common/$PLUGINS_DIR/DomePlugIns/libHuntsmanDome.so" diff --git a/domehunter/protos/generate_grpc_cpp_code.sh b/domehunter/protos/generate_grpc_cpp_code.sh index bf0a3b7..b87ab2b 100755 --- a/domehunter/protos/generate_grpc_cpp_code.sh +++ b/domehunter/protos/generate_grpc_cpp_code.sh @@ -9,8 +9,7 @@ if [ "$1" == "clean" ]; then else HDOME_PATH="$HOME/Documents/REPOS" PROTOS_PATH="$HDOME_PATH/huntsman-dome/domehunter/protos/src/" - PROTO_PATH1="/usr/local/include/google/protobuf/" - PROTO_PATH2="$HDOME_PATH/huntsman-dome/domehunter/protos/src/hx2dome.proto" + PROTO_FILE="$HDOME_PATH/huntsman-dome/domehunter/protos/src/hx2dome.proto" GRPC_CPP_PLUGIN_PATH="$(which grpc_cpp_plugin)" echo -e "Generating GRPC C++ code\n" @@ -18,8 +17,8 @@ else echo -e "protoc -I $PROTOS_PATH --cpp_out=. src/hx2dome.proto\n" protoc -I "$PROTOS_PATH" --cpp_out=. hx2dome.proto - echo -e "protoc -I $PROTOS_PATH --grpc_out=. --proto_path=$PROTO_PATH1 $PROTO_PATH2 --plugin=protoc-gen-grpc=$GRPC_CPP_PLUGIN_PATH\n" - protoc -I "$PROTOS_PATH" --grpc_out=. --proto_path="$PROTO_PATH1" "$PROTO_PATH2" --plugin=protoc-gen-grpc="$GRPC_CPP_PLUGIN_PATH" + echo -e "protoc -I $PROTOS_PATH --grpc_out=. $PROTO_FILE --plugin=protoc-gen-grpc=$GRPC_CPP_PLUGIN_PATH\n" + protoc -I "$PROTOS_PATH" --grpc_out=. "$PROTO_FILE" --plugin=protoc-gen-grpc="$GRPC_CPP_PLUGIN_PATH" echo -e "Moving generated GRPC C++ code to src/\n" mv hx2dome.grpc.pb.cc src/hx2dome.grpc.pb.cpp @@ -34,6 +33,6 @@ else #echo -e "Cleaning out object files.\n" #rm *.o - + echo -e "Done.\n" fi diff --git a/domehunter/protos/generate_grpc_python_code.sh b/domehunter/protos/generate_grpc_python_code.sh index 39ed09e..8c82867 100755 --- a/domehunter/protos/generate_grpc_python_code.sh +++ b/domehunter/protos/generate_grpc_python_code.sh @@ -6,14 +6,13 @@ if [ "$1" == "clean" ]; then else HDOME_PATH="$HOME/Documents/REPOS" PROTOS_PATH="$HDOME_PATH/huntsman-dome/domehunter/protos/src/" - PROTO_PATH1="/usr/local/include/google/protobuf/" - PROTO_PATH2="$HDOME_PATH/huntsman-dome/domehunter/protos/src/hx2dome.proto" + PROTO_FILE="$HDOME_PATH/huntsman-dome/domehunter/protos/src/hx2dome.proto" echo -e "\nGenerating GRPC Python code\n" - echo -e "python -m grpc_tools.protoc -I=$PROTOS_PATH --python_out=. --grpc_python_out=. --proto_path=$PROTO_PATH1 $PROTO_PATH2\n" + echo -e "python -m grpc_tools.protoc -I=$PROTOS_PATH --python_out=. --grpc_python_out=. $PROTO_FILE\n" - python -m grpc_tools.protoc -I=$PROTOS_PATH --python_out=. --grpc_python_out=. --proto_path=$PROTO_PATH1 $PROTO_PATH2 + python -m grpc_tools.protoc -I=$PROTOS_PATH --python_out=. --grpc_python_out=. $PROTO_FILE echo -e "Moving generated GRPC Python code to src/\n" mv *pb2* src/ From ced51decd4e897f58993e351342a49ce6f757cc7 Mon Sep 17 00:00:00 2001 From: fergus Date: Wed, 28 Aug 2019 02:19:43 +1000 Subject: [PATCH 17/18] tidy up --- README.rst | 66 ++++++++++++++++++++++++++++++---- domehunter/protos/Makefile_MAC | 25 +++++-------- requirements.txt | 2 ++ 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 22a888c..a711234 100644 --- a/README.rst +++ b/README.rst @@ -32,19 +32,60 @@ C++/gRPC Component Requirements --------------- -``grpc python`` See instructions `here `_. +``grpc python`` For reference see `here `_. +To install (on MacOS or Linux) the grpc python packages needed run the following:: + + python -m pip install --upgrade pip + python -m pip install grpcio + python -m pip install grpcio-tools + + +``grpc c++`` For reference see `here `_. + +Instructions to install from source on any OS can be found `here `_. + +To install depedencies for a linux OS run the following:: + + [sudo] apt-get install build-essential autoconf libtool pkg-config + [sudo] apt-get install libgflags-dev libgtest-dev + [sudo] apt-get install clang libc++-dev + +To do the same on MacOS (with homebrew installed) run:: + + [sudo] xcode-select --install + brew install autoconf automake libtool shtool + brew install gflags + +Now to build grpc from source on Linux or MacOS run the following:: + + cd /usr/local/bin/ + git clone -b $(curl -L https://grpc.io/release) https://github.com/grpc/grpc + cd grpc/ + git submodule update --init + make + make install + cd third_party/protobuf/ + git submodule update --init --recursive + ./autogen.sh + ./configure + make + make check + make install -``grpc c++`` See instructions `here `_. To install the above on OSX, run:: - pip install --upgrade pip - pip install grpcio - pip install grpcio-tools brew tap grpc/grpc brew install -s -- --with-plugins grpc brew install protobuf + brew install protobuf-c + +However, this may require some editing of the driver makefiles. Specifically +the include and linking flags, as homebrew will place relevant files and +libraries in different locations to the installation from source method +outlined above. The makefiles are written with the installation from source +setup in mind. Getting Started --------------- @@ -64,7 +105,18 @@ directory, with filename ``libHuntsmanDome.so``. | -In order to compile the driver simply run the makefile recipe. +In order to compile the driver simply run the makefile recipe for your OS (LINUX/MAC):: + + cd domehunter/protos/ + make -f Makefile_LINUX + +This will produce a .so file in the protos directory for Linux and a .dylib file for Mac. +This file as well as the ``domelistHuntsmanDome.txt`` file need to be copied into TheSkyX +application directory. This can be done by running the installation script:: + + . TheSkyX_LINUX_plugin_install.sh + +Replace `LINUX` with `MAC` if installing on a MacOS system and vice versa. | @@ -85,7 +137,7 @@ scripts. These can be used to generate the gRPC files within the ``src/`` directory. These scripts contain path variables that may need to be adjusted to your local machine. You shouldn't need to worry about this as the generated files are committed to the repositry and -shouldn't need to be generated. +shouldn't need to be generated (I think...?). The code for the Huntsman dome driver is contained in the ``domehunter/protos/src`` directory. This directory contains both diff --git a/domehunter/protos/Makefile_MAC b/domehunter/protos/Makefile_MAC index f461116..b8ec78c 100644 --- a/domehunter/protos/Makefile_MAC +++ b/domehunter/protos/Makefile_MAC @@ -1,28 +1,21 @@ # Makefile for libHuntsmanDome -CXX = /Library/Developer/CommandLineTools/usr/bin/clang++ +CXX = g++ -CFLAGS = -pipe -O2 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ - -fPIC -DSB_MAC_BUILD -g -I. -I./src/licensedinterfaces\ - -pthread -I/usr/local/include +CFLAGS = -fPIC -O2 -g -DSB_MAC_BUILD -arch i386 -I. -I./src/licensedinterfaces/\ + -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -I/usr/local/include\ -CPPFLAGS = -pipe -O2 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ - -fPIC -DSB_MAC_BUILD -g -I. -I./src/licensedinterfaces\ - -pthread -I/usr/local/include +CPPFLAGS = -fPIC -O2 -g -DSB_MAC_BUILD -I. -I./src/licensedinterfaces/\ + -pthread -I/usr/local/include -I/usr/local/bin/grpc\ -CXXFLAGS += -pipe -stdlib=libc++ -O2 -std=gnu++11 -isysroot\ - /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -fPIC -DSB_MAC_BUILD +CXXFLAGS += -std=c++11 -LDFLAGS = -stdlib=libc++ -headerpad_max_install_names\ - -Wl,-syslibroot,/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\ - -Wl,-rpath,@executable_path/Frameworks\ - -Wl,-rpath,/User/bob/anaconda3/envs/huntsman/lib\ - -single_module -dynamiclib -L/usr/local/lib -lprotobuf -pthread\ - -lgrpc++ -lgrpc++_reflection -ldl -lgrpc +LDFLAGS = -shared -stdlib=libc++ -L/usr/local/lib -lprotobuf -pthread\ + -lgrpc++ -lgrpc++_reflection -ldl RM = rm -f STRIP = strip -TARGET_LIB = libHuntsmanDome.so +TARGET_LIB = libHuntsmanDome.dylib SRCS = src/main.cpp src/x2dome.cpp src/hx2dome.grpc.pb.cpp src/hx2dome.pb.cpp diff --git a/requirements.txt b/requirements.txt index 4716d34..3d11491 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ astropy_helpers +grpcio +grpcio-tools gpiozero smbus # optional for LED use sn3218 # optional for LED use From 1549ee60da8d87c629552001a50052fb7af35071 Mon Sep 17 00:00:00 2001 From: fergus Date: Wed, 28 Aug 2019 02:27:56 +1000 Subject: [PATCH 18/18] tidy2 --- README.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index a711234..db93424 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,12 @@ C++/gRPC Component ================== Requirements ---------------- +------------ ``grpc python`` For reference see `here `_. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -To install (on MacOS or Linux) the grpc python packages needed run the following:: +To install (on MacOS or Linux) the required grpc python packages run the following:: python -m pip install --upgrade pip python -m pip install grpcio @@ -42,16 +43,19 @@ To install (on MacOS or Linux) the grpc python packages needed run the following ``grpc c++`` For reference see `here `_. +------------------------------------------------------------------------------------------------------------------------------------------------------------ + +Detailed instructions to install from source on any OS can be found `here `_. -Instructions to install from source on any OS can be found `here `_. +For convenience a summary of the required steps is given below. -To install depedencies for a linux OS run the following:: +To install depedencies for a linux OS, run the following:: [sudo] apt-get install build-essential autoconf libtool pkg-config [sudo] apt-get install libgflags-dev libgtest-dev [sudo] apt-get install clang libc++-dev -To do the same on MacOS (with homebrew installed) run:: +To do the same on MacOS (with homebrew installed), run:: [sudo] xcode-select --install brew install autoconf automake libtool shtool @@ -74,7 +78,7 @@ Now to build grpc from source on Linux or MacOS run the following:: make install -To install the above on OSX, run:: +Alternatively to installing from source, you can install via homebrew on MacOS by running:: brew tap grpc/grpc brew install -s -- --with-plugins grpc @@ -99,9 +103,10 @@ The files for compilation and installation are found in the * ``Makefile`` The first two are files are used to install the compiled c++ -driver. You should be able to simply run the shell script once -the driver is compiled and located in the ``domehunter/protos/`` -directory, with filename ``libHuntsmanDome.so``. +driver into TheSkyX application directory. You should be +able to simply run the shell script once the driver is compiled +and located in the ``domehunter/protos/`` directory, with +filename ``libHuntsmanDome.so``. | @@ -111,7 +116,7 @@ In order to compile the driver simply run the makefile recipe for your OS (LINUX make -f Makefile_LINUX This will produce a .so file in the protos directory for Linux and a .dylib file for Mac. -This file as well as the ``domelistHuntsmanDome.txt`` file need to be copied into TheSkyX +This file, as well as the ``domelistHuntsmanDome.txt`` file need to be copied into TheSkyX application directory. This can be done by running the installation script:: . TheSkyX_LINUX_plugin_install.sh