diff --git a/README.rst b/README.rst index cad45a4..db93424 100644 --- a/README.rst +++ b/README.rst @@ -26,31 +26,73 @@ 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 ---------------- +------------ -``grpc python`` See instructions `here `_. -``grpc c++`` See instructions `here `_. +``grpc python`` For reference see `here `_. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -Getting Started ---------------- +To install (on MacOS or Linux) the required grpc python packages run the following:: -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, + python -m pip install --upgrade pip + python -m pip install grpcio + python -m pip install grpcio-tools -* ``main.cpp`` - establishes main library to X2 driver (mostly copy/paste from example) -* ``main.h`` - header for main.cpp -* ``x2dome.cpp`` - the library code that serves the RPC from TSX to python server -* ``x2dome.h`` - header for x2dome.cpp -* ``hx2dome.proto`` - language agnostic RPC definitions used by everthing -* ``hx2dome.proto_server.py`` - python server that receives RPC from TSX -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. +``grpc c++`` For reference see `here `_. +------------------------------------------------------------------------------------------------------------------------------------------------------------ + +Detailed 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:: + + [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 + + +Alternatively to installing from source, you can install via homebrew on MacOS by running:: + + 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 +--------------- The files for compilation and installation are found in the ``domehunter/protos/`` directory. The relevant files are, @@ -61,13 +103,25 @@ 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``. | -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. | @@ -88,10 +142,50 @@ 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 +human written files and files automatically generated by gRPC +tools. The human written files are, + +* ``main.cpp`` - establishes main library to X2 driver (mostly copy/paste from example) +* ``main.h`` - header for main.cpp +* ``x2dome.cpp`` - the library code that serves the RPC from TSX to python server +* ``x2dome.h`` - header for x2dome.cpp +* ``hx2dome.proto`` - language agnostic RPC definitions used by everthing +* ``hx2dome.proto_server.py`` - python server that receives RPC from TSX + +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 +============================ +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 +196,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 edd0863..fccf971 100644 --- a/domehunter/__init__.py +++ b/domehunter/__init__.py @@ -1,20 +1,33 @@ """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, Device +import sys +import time +import warnings + +from gpiozero import Device, DigitalInputDevice, DigitalOutputDevice from gpiozero.pins.mock import MockFactory from ._astropy_init import * + +# 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.") +except: + warnings.warn( + "Something went wrong in importing sn3218, status lights unlikely to work.") + + # ---------------------------------------------------------------------------- + # 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" @@ -31,52 +44,175 @@ class UnsupportedPythonError(Exception): pass -class X2DomeRPC(): - """Dummy class until real RPC class is available.""" +class Dome(): + """ + Interface to dome control raspberry pi GPIO. - def __init__(self, *args, **kwargs): - """Create dummy class until real RPC class is available.""" + 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. -class Dome(X2DomeRPC): + 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/ """ - 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. - """ + def __init__(self, testing=True, debug_lights=False, *args, **kwargs): + """ + Initialize raspberry pi GPIO environment. + + 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 when + 'testing=False'. With 'debug_lights=True', LEDs on AutomationHat will + be used to indicate current state. + + 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 (`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 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 - def __init__(self, testing=True, *args, **kwargs): - """Initialize raspberry pi GPIO environment.""" if testing: # Set the default pin factory to a mock factory Device.pin_factory = MockFactory() - ENCODER_PIN_NUMBER = 7 - HOME_SENSOR_PIN_NUMBER = 11 + # 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) else: - """ Do not change until you're sure!!! """ - # https://pinout.xyz/pinout/automation_hat - ENCODER_PIN_NUMBER = None - HOME_SENSOR_PIN_NUMBER = None - - self.dome_status = "unknown" - + # set the timeout length variable to None for non testing mode + WAIT_TIMEOUT = None + + # 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 + # 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 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, + 'warn': 4, + 'input_1': 5, + 'input_2': 6, + 'input_3': 7, + '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, + '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 + self.debug_lights = debug_lights + self._dome_status = "unknown" + self.dome_az = None + + # create a instance variable to track the dome motor encoder ticks self.encoder_count = 0 - self.encoder = DigitalInputDevice(ENCODER_PIN_NUMBER) + # bounce_time settings gives the time in seconds that the device will + # ignore additional activation signals + 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 + # set dummy value initially to force a rotation calibration run + self.az_per_tick = None - self._at_home = False - self.home_sensor = DigitalInputDevice(HOME_SENSOR_PIN_NUMBER) - # _set_not_home function is run when home sensor is NOT being triggered + 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 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 -################################################################################################## + # 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_CW_relay = DigitalOutputDevice( + DIRECTION_RELAY_PIN_NUMBER, initial_value=False) + # 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_normally_closed']) + self._turn_led_on(leds=['relay_1_normally_closed']) + +############################################################################### # Properties -################################################################################################## +############################################################################### @property def is_home(self): @@ -86,70 +222,384 @@ def is_home(self): @property def status(self): """Return a text string describing dome rotators current status.""" - pass + return self._dome_status - -################################################################################################## +############################################################################### # Methods -################################################################################################## +############################################################################### """These map directly onto the AbstractMethods created by RPC.""" def abort(self): - """Stop everything.""" - pass - - def getAzEl(self): - """Return AZ and Elevation.""" - pass - - def start_daemon(self): - """Maybe start the RCP daemon here?.""" - raise NotImplementedError - - def halt_daemon(self): - """Maybe start the RCP daemon here?.""" - raise NotImplementedError + """ + 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() + + def getAz(self): + """ + Return current Azimuth of the Dome. + + Returns + ------- + float + The Dome azimuth in degrees. + + """ + if self.dome_az is None: + 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. + + Parameters + ---------- + az : float + Desired dome azimuth position in degrees. + + """ + if self.dome_az is None: + 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 + 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() + # 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) + 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) + + # if updated delta_az is negative, direction is CCW + 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_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) + 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 + 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) + + 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) + 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() + self.encoder_count = 0 -################################################################################################## + # 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: + # 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) + +############################################################################### # Private Methods -################################################################################################## +############################################################################### 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, device=None): - print(f"{device} activated _increment_count") + def _increment_count(self): + """ + 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) + self._turn_led_off(leds=['input_1']) if self.current_direction == "CW": 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 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) - - return cmd_status - - def _move_cw(self, degrees): - print(f"Sending GPIO move_cw({degrees}) command.") + def _az_to_ticks(self, az): + """ + 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). + + 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. + + 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() + # 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_CW_relay.on() + # update the debug LEDs + if self.last_direction == "CCW": + 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_normally_open']) + self._turn_led_off(leds=['relay_1_normally_closed']) cmd_status = True return cmd_status - def _move_ccw(self, degrees): - print(f"Sending GPIO move_ccw({degrees}) command.") + def _move_ccw(self): + """ + 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() + # 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_CW_relay.off() + # update the debug LEDs + if self.last_direction == "CW": + 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_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. + """ + self.rotation_relay.off() + # update the debug LEDs + 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. + """ + 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() + # test_mode_delay_duration is set so that it will always exceed + # the set bounce_time + # of the pins + time.sleep(self.test_mode_delay_duration) + self.encoder_pin.drive_high() + 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 + + 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 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. + + 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: + 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/domehunter/protos/Makefile b/domehunter/protos/Makefile_LINUX similarity index 66% rename from domehunter/protos/Makefile rename to domehunter/protos/Makefile_LINUX index 9ef03f2..e0f3ac5 100644 --- a/domehunter/protos/Makefile +++ b/domehunter/protos/Makefile_LINUX @@ -1,16 +1,16 @@ # 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\ - -L/usr/local/lib -lprotobuf -pthread -lgrpc++ -pthread +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 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/Makefile_MAC b/domehunter/protos/Makefile_MAC new file mode 100644 index 0000000..b8ec78c --- /dev/null +++ b/domehunter/protos/Makefile_MAC @@ -0,0 +1,36 @@ +# Makefile for libHuntsmanDome + +CXX = g++ + +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 = -fPIC -O2 -g -DSB_MAC_BUILD -I. -I./src/licensedinterfaces/\ + -pthread -I/usr/local/include -I/usr/local/bin/grpc\ + +CXXFLAGS += -std=c++11 + +LDFLAGS = -shared -stdlib=libc++ -L/usr/local/lib -lprotobuf -pthread\ + -lgrpc++ -lgrpc++_reflection -ldl + +RM = rm -f +STRIP = strip +TARGET_LIB = libHuntsmanDome.dylib + +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_plugin_install.sh b/domehunter/protos/TheSkyX_LINUX_plugin_install.sh similarity index 93% rename from domehunter/protos/TheSkyX_plugin_install.sh rename to domehunter/protos/TheSkyX_LINUX_plugin_install.sh index d9ee590..0c4d36c 100755 --- a/domehunter/protos/TheSkyX_plugin_install.sh +++ b/domehunter/protos/TheSkyX_LINUX_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/" +# 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_MAC_plugin_install.sh b/domehunter/protos/TheSkyX_MAC_plugin_install.sh new file mode 100755 index 0000000..cf9b285 --- /dev/null +++ b/domehunter/protos/TheSkyX_MAC_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 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" +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| 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/ 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..3d11491 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ astropy_helpers +grpcio +grpcio-tools gpiozero +smbus # optional for LED use +sn3218 # optional for LED use 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