Exemplo n.º 1
0
class DirectWeather:
    """
    the class DirectWeather inherits all information and handling of the environment device

        >>> DirectWeather(host=None,
        >>>         name=''
        >>>         )
    """

    __all__ = [
        'DirectWeather',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app=None):

        self.app = app

        # minimum set for driver package built in
        self.framework = None
        self.run = {'built-in': self}
        self.name = ''

    @staticmethod
    def startCommunication():
        return True

    @staticmethod
    def stopCommunication():
        return True
Exemplo n.º 2
0
def module_setup_teardown(qtbot):
    global ui, widget, Test, Test1, app

    class Test1(QObject):
        mount = Mount()
        update1s = pyqtSignal()
        update10s = pyqtSignal()
        threadPool = QThreadPool()

    class Test(QObject):
        config = {'mainW': {}}
        threadPool = QThreadPool()
        update1s = pyqtSignal()
        message = pyqtSignal(str, int)
        mount = Mount()
        mount.obsSite.location = Topos(latitude_degrees=20,
                                       longitude_degrees=10,
                                       elevation_m=500)
        sensorWeather = SensorWeather(app=Test1())
        onlineWeather = OnlineWeather(app=Test1())
        directWeather = DirectWeather(app=Test1())
        skymeter = Skymeter(app=Test1())
        cover = FlipFlat(app=Test1())
        filter = Filter(app=Test1())
        camera = Camera(app=Test1())
        focuser = Focuser(app=Test1())
        dome = Dome(app=Test1())
        power = PegasusUPB(app=Test1())
        astrometry = Astrometry(app=Test1())
        relay = KMRelay()
        measure = MeasureData(app=Test1())
        remote = Remote(app=Test1())
        telescope = Telescope(app=Test1())

    widget = QWidget()
    ui = Ui_MainWindow()
    ui.setupUi(widget)

    app = SettDevice(app=Test(), ui=ui,
                     clickable=MWidget().clickable,
                     change=MWidget().changeStyleDynamic)
    app.close = MWidget().close
    app.deleteLater = MWidget().deleteLater
    app.deviceStat = dict()
    app.log = CustomLogger(logging.getLogger(__name__), {})
    app.threadPool = QThreadPool()
    app.config = dict()
    app.BACK_NORM = '#000000'

    qtbot.addWidget(app)

    yield

    del widget, ui, Test, Test1, app
Exemplo n.º 3
0
class Worker(PyQt5.QtCore.QRunnable):
    """
    The Worker class offers a generic interface to allow any function to be executed as
    a thread in an threadpool
    """

    __all__ = ['Worker', 'run']

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        # the worker signal must not be a class variable, but instance otherwise
        # we get trouble when having multiple threads running
        self.signals = WorkerSignals()

    @PyQt5.QtCore.pyqtSlot()
    def run(self):
        """
        runs an arbitrary methods with it's parameters and catches the result

        :return: nothing, but sends results and status as signals
        """

        try:
            result = self.fn(*self.args, **self.kwargs)
        except Exception:
            # as we want to send a clear message to the log file
            exc_type, exc_value, exc_traceback = sys.exc_info()
            tb = exc_traceback

            # moving toward the end of the trace
            while tb.tb_next is not None:
                tb = tb.tb_next

            # getting data out for processing
            file = tb.tb_frame.f_code.co_filename
            line = tb.tb_frame.f_lineno

            errorString = f'{file}, line {line} {exc_value}'
            self.log.critical(errorString)
            self.signals.error.emit(errorString)
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()
Exemplo n.º 4
0
class SkymeterIndi(IndiClass):
    """
    the class SkymeterIndi inherits all information and handling of the Skymeter device

        >>> s = SkymeterIndi(app=None)
    """

    __all__ = [
        'SkymeterIndi',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1 seconds for setting indi server
    UPDATE_RATE = 5

    def __init__(self, app=None, signals=None, data=None):
        super().__init__(app=app, data=data)

        self.signals = signals
        self.data = data

    def setUpdateConfig(self, deviceName):
        """
        _setUpdateRate corrects the update rate of weather devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """

        if deviceName != self.name:
            return False

        if self.device is None:
            return False

        update = self.device.getNumber('WEATHER_UPDATE')

        if 'PERIOD' not in update:
            return False

        if update.get('PERIOD', 0) == self.UPDATE_RATE:
            return True

        update['PERIOD'] = self.UPDATE_RATE
        suc = self.client.sendNewNumber(deviceName=deviceName,
                                        propertyName='WEATHER_UPDATE',
                                        elements=update)
        return suc
Exemplo n.º 5
0
def module_setup_teardown(qtbot):
    global ui, widget, Test, Test1, app

    class Test1(QObject):
        mount = Mount()
        update10s = pyqtSignal()
        threadPool = QThreadPool()

    class Test(QObject):
        config = {'mainW': {}}
        threadPool = QThreadPool()
        update1s = pyqtSignal()
        update30m = pyqtSignal()
        message = pyqtSignal(str, int)
        mount = Mount()
        mount.obsSite.location = Topos(latitude_degrees=20,
                                       longitude_degrees=10,
                                       elevation_m=500)
        sensorWeather = SensorWeather(app=Test1())
        onlineWeather = OnlineWeather(app=Test1())
        skymeter = Skymeter(app=Test1())

    widget = QWidget()
    ui = Ui_MainWindow()
    ui.setupUi(widget)

    app = EnvironGui(app=Test(),
                     ui=ui,
                     clickable=MWidget().clickable,
                     change=MWidget().changeStyleDynamic)
    app.close = MWidget().close
    app.deleteLater = MWidget().deleteLater
    app.deviceStat = dict()
    app.log = CustomLogger(logging.getLogger(__name__), {})
    app.threadPool = QThreadPool()

    qtbot.addWidget(app)

    yield

    del widget, ui, Test, Test1, app
Exemplo n.º 6
0
class QAwesomeTooltipEventFilter(PyQt5.QtCore.QObject):
    """
    Tooltip-specific event filter dramatically improving the tooltips of all
    widgets for which this filter is installed.

    Motivation
    ----------
    **Rich text tooltips** (i.e., tooltips containing one or more HTML-like
    tags) are implicitly wrapped by Qt to the width of their parent windows and
    hence typically behave as expected.

    **Plaintext tooltips** (i.e., tooltips containing no such tags), however,
    are not. For unclear reasons, plaintext tooltips are implicitly truncated to
    the width of their parent windows. The only means of circumventing this
    obscure constraint is to manually inject newlines at the appropriate
    80-character boundaries of such tooltips -- which has the distinct
    disadvantage of failing to scale to edge-case display and device
    environments (e.g., high-DPI). Such tooltips *cannot* be guaranteed to be
    legible in the general case and hence are blatantly broken under *all* Qt
    versions to date. This is a `well-known long-standing issue <issue_>`__ for
    which no official resolution exists.

    This filter globally addresses this issue by implicitly converting *all*
    intercepted plaintext tooltips into rich text tooltips in a general-purpose
    manner, thus wrapping the former exactly like the latter. To do so, this
    filter (in order):

    #. Auto-detects whether the:

       * Current event is a :class:`QEvent.ToolTipChange` event.
       * Current widget has a **non-empty plaintext tooltip**.

    #. When these conditions are satisfied:

       #. Escapes all HTML syntax in this tooltip (e.g., converting all ``&``
          characters to ``&amp;`` substrings).
       #. Embeds this tooltip in the Qt-specific ``<qt>...</qt>`` tag, thus
          implicitly converting this plaintext tooltip into a rich text tooltip.

    .. _issue:
        https://bugreports.qt.io/browse/QTBUG-41051
    """

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def eventFilter(self, widget, event):
        """
        Tooltip-specific event filter handling the passed Qt object and event.
        """

        # If this is a tooltip event...
        if event.type() == PyQt5.QtCore.QEvent.ToolTipChange:
            # If the target Qt object containing this tooltip is *NOT* a widget,
            # raise a human-readable exception. While this should *NEVER* be the
            # case, edge cases are edge cases because they sometimes happen.
            if not isinstance(widget, PyQt5.QtWidgets.QWidget):
                self.log.error('QObject "{}" not a widget.'.format(widget))

            # Tooltip for this widget if any *OR* the empty string otherwise.
            tooltip = widget.toolTip()

            # If this tooltip is both non-empty and not already rich text...
            if tooltip == '<html><head/><body><p><br/></p></body></html>':
                widget.setToolTip(None)
                return True
            elif tooltip and not PyQt5.QtCore.Qt.mightBeRichText(tooltip):
                # Convert this plaintext tooltip into a rich text tooltip by:
                #
                # * Escaping all HTML syntax in this tooltip.
                # * Embedding this tooltip in the Qt-specific "<qt>...</qt>" tag.
                tooltip = '<qt>{}</qt>'.format(html.escape(tooltip))

                # Replace this widget's non-working plaintext tooltip with this
                # working rich text tooltip.
                widget.setToolTip(tooltip)

                # Notify the parent event handler this event has been handled.
                return True
        elif event.type() == PyQt5.QtCore.QEvent.ToolTip:
            if not isinstance(widget, PyQt5.QtWidgets.QWidget):
                self.log.error('QObject "{}" not a widget.'.format(widget))

            # Tooltip for this widget if any *OR* the empty string otherwise.
            tooltip = widget.toolTip()
            if tooltip == '<html><head/><body><p><br/></p></body></html>':
                widget.setToolTip(None)
                return True

        # Else, defer to the default superclass handling of this event.
        return super().eventFilter(widget, event)
Exemplo n.º 7
0
class DataPoint(object):
    """
    The class Data inherits all information and handling of modeldata data and other
    attributes. this includes horizon data, model points data and their persistence

        >>> data = DataPoint(
        >>>                  app=None,
        >>>                  mwGlob=mwglob,
        >>>                  )
    """

    __all__ = [
        'DataPoint',
        'genGreaterCircle',
        'genGrid',
        'genInitial',
        'loadBuildP',
        'saveBuildP',
        'clearPoints'
        'deleteBelowHorizon',
        'sort',
        'loadHorizonP',
        'saveHorizonP',
        'clearHorizonP',
        'generateCelestialEquator',
        'generateDSOPath',
        'generateGoldenSpiral',
        'genAlign',
        'hip',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # data for generating greater circles, dec and step only for east, west is reversed
    DEC = {
        'min': [-15, 0, 15, 30, 45, 60, 75],
        'norm': [-15, 0, 15, 30, 45, 60, 75],
        'med': [-15, -5, 5, 15, 25, 40, 55, 70, 85],
        'max': [-15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85],
    }
    STEP = {
        'min': [15, -15, 15, -15, 15, -30, 30],
        'norm': [10, -10, 10, -10, 10, -20, 20],
        'med': [10, -10, 10, -10, 10, -10, 10, -30, 30],
        'max': [10, -10, 10, -10, 10, -10, 10, -10, 10, -30, 30],
    }
    START = {
        'min': [-120, -5, -120, -5, -120, -5, -120, 5, 120, 5, 120, 5, 120, 5],
        'norm':
        [-120, -5, -120, -5, -120, -5, -120, 5, 120, 5, 120, 5, 120, 5],
        'med': [
            -120, -5, -120, -5, -120, -5, -120, -5, -120, 5, 120, 5, 120, 5,
            120, 5, 120, 5
        ],
        'max': [
            -120, -5, -120, -5, -120, -5, -120, -5, -120, -5, -120, 5, 120, 5,
            120, 5, 120, 5, 120, 5, 120, 5
        ],
    }
    STOP = {
        'min': [0, -120, 0, -120, 0, -120, 0, 120, 0, 120, 0, 120, 0, 120],
        'norm': [0, -120, 0, -120, 0, -120, 0, 120, 0, 120, 0, 120, 0, 120],
        'med': [
            0, -120, 0, -120, 0, -120, 0, -120, 0, 120, 0, 120, 0, 120, 0, 120,
            0, 120, 0
        ],
        'max': [
            0, -120, 0, -120, 0, -120, 0, -120, 0, -120, 0, 120, 0, 120, 0,
            120, 0, 120, 0, 120, 0, 120, 0
        ],
    }

    def __init__(
        self,
        app=None,
        mwGlob=None,
    ):

        self.mwGlob = mwGlob
        self.app = app
        self._horizonP = [(0, 0), (0, 360)]
        self._buildP = list()

    @property
    def buildP(self):
        return self._buildP

    @buildP.setter
    def buildP(self, value):
        if not isinstance(value, list):
            self._buildP = list()
            return
        if not all([isinstance(x, tuple) for x in value]):
            self.log.warning('malformed value: {0}'.format(value))
            self._buildP = list()
            return
        self._buildP = value

    def addBuildP(self, value=None, position=None):
        """
        addBuildP extends the list of modeldata points. the new point could be added at the end
        of the list (default) or in any location in the list.

        :param value: value to be inserted
        :param position: position in list
        :return:
        """

        if value is None:
            return False
        if not isinstance(value, tuple):
            self.log.warning('malformed value: {0}'.format(value))
            return False
        if len(value) != 2:
            self.log.warning('malformed value: {0}'.format(value))
            return False
        if position is None:
            position = len(self._buildP)
        if not isinstance(position, (int, float)):
            self.log.warning('malformed position: {0}'.format(position))
            return False
        if self.app.mount.setting.horizonLimitHigh is not None:
            high = self.app.mount.setting.horizonLimitHigh
        else:
            high = 90
        if self.app.mount.setting.horizonLimitLow is not None:
            low = self.app.mount.setting.horizonLimitLow
        else:
            low = 0
        if value[0] > high:
            return False
        if value[0] < low:
            return False
        position = int(position)
        position = min(len(self._buildP), position)
        position = max(0, position)
        self._buildP.insert(position, value)
        return True

    def delBuildP(self, position):
        """
        delBuildP deletes one point from the modeldata points list at the given index.

        :param position:
        :return:
        """

        if not isinstance(position, (int, float)):
            self.log.warning('malformed position: {0}'.format(position))
            return False
        position = int(position)
        if position < 0 or position > len(self._buildP) - 1:
            self.log.warning('invalid position: {0}'.format(position))
            return False
        self._buildP.pop(position)
        return True

    def clearBuildP(self):
        self._buildP = list()

    def checkHorizonBoundaries(self):
        if self._horizonP[0] != (0, 0):
            self._horizonP.insert(0, (0, 0))
        horMax = len(self._horizonP)
        if self._horizonP[horMax - 1] != (0, 360):
            self._horizonP.insert(horMax, (0, 360))

    @property
    def horizonP(self):
        self._horizonP = sorted(self._horizonP, key=lambda x: x[1])
        self.checkHorizonBoundaries()
        return self._horizonP

    @horizonP.setter
    def horizonP(self, value):
        if not isinstance(value, list):
            self.clearHorizonP()
            return
        if not all([isinstance(x, tuple) for x in value]):
            self.log.warning('malformed value: {0}'.format(value))
            self.clearHorizonP()
            return
        self._horizonP = value
        self.checkHorizonBoundaries()

    @staticmethod
    def checkFormat(value):
        if not isinstance(value, list):
            return False
        if not all([isinstance(x, list) for x in value]):
            return False
        if not all([len(x) == 2 for x in value]):
            return False
        return True

    def addHorizonP(self, value=None, position=None):
        """
        addHorizonP extends the list of modeldata points. the new point could be added at the
        end of the list (default) or in any location in the list.

        :param value:
        :param position:
        :return:
        """

        if value is None:
            return False
        if not isinstance(value, tuple):
            self.log.warning('malformed value: {0}'.format(value))
            return False
        if len(value) != 2:
            self.log.warning('malformed value: {0}'.format(value))
            return False
        if position is None:
            position = len(self._horizonP)
        if not isinstance(position, (int, float)):
            self.log.warning('malformed position: {0}'.format(position))
            return False
        position = int(position)
        position = min(len(self._horizonP), position)
        position = max(0, position)
        self._horizonP.insert(position, value)
        return True

    def delHorizonP(self, position):
        """
        delHorizonP deletes one point from the modeldata points list at the given index.

        :param position:
        :return:
        """

        if not isinstance(position, (int, float)):
            self.log.warning('malformed position: {0}'.format(position))
            return False
        position = int(position)
        if position < 0 or position > len(self._horizonP) - 1:
            self.log.warning('invalid position: {0}'.format(position))
            return False
        if self._horizonP[position] == (0, 0):
            return False
        if self._horizonP[position] == (0, 360):
            return False
        self._horizonP.pop(position)
        return True

    def clearHorizonP(self):
        self._horizonP = [(0, 0), (0, 360)]

    def isAboveHorizon(self, point):
        """
        isAboveHorizon calculates for a given point the relationship to the actual horizon
        and determines if this point is above the horizon line. for that there will be a
        linear interpolation for the horizon line points.

        :param point:
        :return:
        """

        if point[1] > 360:
            point = (point[0], 360)
        if point[1] < 0:
            point = (point[0], 0)
        x = range(0, 361)
        y = np.interp(
            x,
            [i[1] for i in self._horizonP],
            [i[0] for i in self._horizonP],
        )
        if point[0] > y[int(point[1])]:
            return True
        else:
            return False

    def isCloseMeridian(self, point):
        """
        isCloseMeridian check if a point is in inner to meridian slew limit

        :param point:
        :return: status
        """

        slew = self.app.mount.setting.meridianLimitSlew
        track = self.app.mount.setting.meridianLimitTrack

        if slew is None or track is None:
            return True

        value = max(slew, track)

        lower = 180 - value
        upper = 180 + value
        if lower < point[1] < upper:
            return False
        else:
            return True

    def deleteBelowHorizon(self):
        """
        deleteBelowHorizon deletes all points which are below horizon line

        :return: true for test purpose
        """
        self._buildP = [x for x in self._buildP if self.isAboveHorizon(x)]
        return True

    def deleteCloseMeridian(self):
        """
        deleteCloseMeridian deletes all points which are too close to the meridian and
        therefore need to flip all the time

        :return: true for test purpose
        """
        self._buildP = [x for x in self._buildP if self.isCloseMeridian(x)]
        return True

    def sort(self, eastwest=False, highlow=False):
        """

        :param eastwest: flag if to be sorted east - west
        :param highlow:  flag if sorted high low altitude
        :return: true for test purpose
        """

        if eastwest and highlow:
            return False
        if not eastwest and not highlow:
            return False

        east = [x for x in self._buildP if x[1] <= 180]
        west = [x for x in self._buildP if x[1] > 180]

        if eastwest:
            east = sorted(east, key=lambda x: -x[1])
            west = sorted(west, key=lambda x: -x[1])

        if highlow:
            east = sorted(east, key=lambda x: -x[0])
            west = sorted(west, key=lambda x: -x[0])

        self._buildP = east + west
        return True

    def loadBuildP(self, fileName=None):
        """
        loadBuildP loads a modeldata pints file and stores the data in the buildP list.
        necessary conversion are made.

        :param fileName: name of file to be handled
        :return: success
        """

        if fileName is None:
            return False
        fileName = self.mwGlob['configDir'] + '/' + fileName + '.bpts'
        if not os.path.isfile(fileName):
            return False

        try:
            with open(fileName, 'r') as handle:
                value = json.load(handle)
        except Exception as e:
            self.log.warning('Cannot load: {0}, error: {1}'.format(
                fileName, e))
            return False

        suc = self.checkFormat(value)
        if not suc:
            self.clearBuildP()
            return False
        # json makes list out of tuple, was to be reversed
        value = [tuple(x) for x in value]
        self._buildP = value
        return True

    def saveBuildP(self, fileName=None):
        """
        saveBuildP saves the actual modeldata points list in a file in json dump format

        :param fileName: name of file to be handled
        :return: success
        """

        if fileName is None:
            return False
        fileName = self.mwGlob['configDir'] + '/' + fileName + '.bpts'
        with open(fileName, 'w') as handle:
            json.dump(self.buildP, handle, indent=4)
        return True

    def loadHorizonP(self, fileName=None):
        """
        loadHorizonP loads a modeldata pints file and stores the data in the buildP list.
        necessary conversion are made.

        :param fileName: name of file to be handled
        :return: success
        """

        if fileName is None:
            return False
        fileName = self.mwGlob['configDir'] + '/' + fileName + '.hpts'
        if not os.path.isfile(fileName):
            return False

        try:
            with open(fileName, 'r') as handle:
                value = json.load(handle)
        except Exception as e:
            self.log.warning('Cannot load: {0}, error: {1}'.format(
                fileName, e))
            return False

        suc = self.checkFormat(value)
        if not suc:
            self.clearHorizonP()
            return False
        # json makes list out of tuple, was to be reversed
        value = [tuple(x) for x in value]
        self._horizonP = value
        return True

    @staticmethod
    def checkBoundaries(points):
        """
        checkBoundaries removes point 0,0 and 0, 360 if present.

        :param points:
        :return: points
        """

        if points[0] == (0, 0):
            del points[0]
        if points[len(points) - 1] == (0, 360):
            del points[-1]
        return points

    def saveHorizonP(self, fileName=None):
        """
        saveHorizonP saves the actual modeldata points list in a file in json dump format

        :param fileName: name of file to be handled
        :return: success
        """

        if fileName is None:
            return False
        fileName = self.mwGlob['configDir'] + '/' + fileName + '.hpts'
        points = self.checkBoundaries(self.horizonP)
        with open(fileName, 'w') as handle:
            json.dump(points, handle, indent=4)
        return True

    def genHaDecParams(self, selection):
        """
        genHaDecParams selects the parameters for generating the boundaries for next
        step processing greater circles. the parameters are sorted for different targets
        actually for minimum slew distance between the points. defined is only the east
        side of data, the west side will be mirrored to the east one.

        :param selection: type of model we would like to use
        :return: yield tuple of dec value and step, start and stop for range
        """

        if selection not in self.DEC or selection not in self.STEP:
            return
        eastDec = self.DEC[selection]
        westDec = list(reversed(eastDec))
        decL = eastDec + westDec

        eastStepL = self.STEP[selection]
        westStepL = list(reversed(eastStepL))
        stepL = eastStepL + westStepL
        startL = self.START[selection]
        stopL = self.STOP[selection]

        for dec, step, start, stop in zip(decL, stepL, startL, stopL):
            yield dec, step, start, stop

    def genGreaterCircle(self, selection='norm'):
        """
        genGreaterCircle takes the generated boundaries for the rang routine and
        transforms ha, dec to alt az. reasonable values for the alt az values
        are 5 to 85 degrees.

        :param selection:
        :return: yields alt, az tuples which are above horizon
        """

        if not self.app.mount.obsSite.location:
            return False

        self.clearBuildP()
        lat = self.app.mount.obsSite.location.latitude.degrees
        for dec, step, start, stop in self.genHaDecParams(selection):
            for ha in range(start, stop, step):
                alt, az = HaDecToAltAz(ha / 10, dec, lat)
                # only values with above horizon = 0

                if 5 <= alt <= 85 and 2 < az < 358:
                    alt += random.uniform(-2, 2)
                    self.addBuildP((alt, az))
        return True

    @staticmethod
    def genGridGenerator(eastAlt, westAlt, minAz, stepAz, maxAz):
        """
        genGridGenerator generates the point values out of the given ranges of altitude
        and azimuth

        :param eastAlt:
        :param westAlt:
        :param minAz:
        :param stepAz:
        :param maxAz:
        :return:
        """

        for i, alt in enumerate(eastAlt):
            if i % 2:
                for az in range(minAz, 180, stepAz):
                    yield (alt, az)
            else:
                for az in range(180 - minAz, 0, -stepAz):
                    yield (alt, az)
        for i, alt in enumerate(westAlt):
            if i % 2:
                for az in range(180 + minAz, 360, stepAz):
                    yield (alt, az)
            else:
                for az in range(maxAz, 180, -stepAz):
                    yield (alt, az)

    def genGrid(self, minAlt=5, maxAlt=85, numbRows=5, numbCols=6):
        """
        genGrid generates a grid of points and transforms ha, dec to alt az. with given
        limits in alt, the min and max will be used as a hard condition. on az there is
        not given limit, therefore a split over the whole space (omitting the meridian)
        is done. the simplest way to avoid hitting the meridian is to enforce the number
        of cols to be a factor of 2. reasonable values for the grid are 5 to 85 degrees.
        defined is only the east side of data, the west side will be mirrored to the
        east one.
            the number of rows is 2 < x < 8
            the number of columns is 2 < x < 15

        :param minAlt: altitude min
        :param maxAlt: altitude max
        :param numbRows: numbRows
        :param numbCols: numbCols
        :return: yields alt, az tuples which are above horizon
        """

        if not 5 <= minAlt <= 85:
            return False
        if not 5 <= maxAlt <= 85:
            return False
        if not maxAlt > minAlt:
            return False
        if not 1 < numbRows < 9:
            return False
        if not 1 < numbCols < 16:
            return False
        if numbCols % 2:
            return False

        minAlt = int(minAlt)
        maxAlt = int(maxAlt)
        numbCols = int(numbCols)
        numbRows = int(numbRows)

        stepAlt = int((maxAlt - minAlt) / (numbRows - 1))
        eastAlt = list(range(minAlt, maxAlt + 1, stepAlt))
        westAlt = list(reversed(eastAlt))

        stepAz = int(360 / numbCols)
        minAz = int(180 / numbCols)
        maxAz = 360 - minAz

        self.clearBuildP()
        for point in self.genGridGenerator(eastAlt, westAlt, minAz, stepAz,
                                           maxAz):
            self.addBuildP(point)
        return True

    def genAlign(self, altBase=30, azBase=10, numberBase=3):
        """
        genAlign generates a number of initial points for the first step of modeling. it
        adjusts the first align point in a matter, that the starting point is the closest
        to az = 0.

        :param altBase:
        :param azBase:
        :param numberBase:
        :return: yields alt, az tuples which are above horizon
        """

        if not 5 <= altBase <= 85:
            return False
        if not 2 < numberBase < 11:
            return False
        if not 0 <= azBase < 360:
            return False

        stepAz = int(360 / numberBase)
        altBase = int(altBase)
        azBase = int(azBase) % stepAz
        numberBase = int(numberBase)

        self.clearBuildP()
        for i in range(0, numberBase):
            az = azBase + i * stepAz
            self.addBuildP((altBase, az % 360))
        return True

    def generateCelestialEquator(self):
        """
        generateCelestialEquator calculates a line for greater circles like a celestial
        equator for showing the paths in the hemisphere window.

        :return: celestial equator
        """

        celestialEquator = list()
        if not self.app.mount.obsSite.location:
            return celestialEquator

        lat = self.app.mount.obsSite.location.latitude.degrees
        for dec in range(-15, 90, 15):
            for ha in range(-119, 120, 2):
                az, alt = HaDecToAltAz(ha / 10, dec, lat)
                if alt > 0:
                    celestialEquator.append((az, alt))
        for ha in range(-115, 120, 10):
            for dec in range(-90, 90, 2):
                az, alt = HaDecToAltAz(ha / 10, dec, lat)
                if alt > 0:
                    celestialEquator.append((az, alt))
        return celestialEquator

    def generateDSOPath(self,
                        ra=0,
                        dec=0,
                        timeJD=0,
                        location=None,
                        numberPoints=0,
                        duration=0,
                        timeShift=0):
        """
        generateDSOPath calculates a list of model points along the desired path beginning
        at ra, dec coordinates, which is in time duration hours long and consists of
        numberPoints model points. TimeShift moves the pearl of points to an earlier or
        later point in time.

        :param ra:
        :param dec:
        :param timeJD:
        :param location:
        :param numberPoints:
        :param duration:
        :param timeShift:
        :return: True for test purpose
        """

        if numberPoints < 1:
            return False
        if duration == 0:
            return False
        if location is None:
            return False

        numberPoints = int(numberPoints)

        self.clearBuildP()
        for i in range(0, numberPoints):
            startPoint = ra.hours - i * duration / numberPoints - timeShift
            raCalc = skyfield.api.Angle(hours=startPoint)
            az, alt = transform.J2000ToAltAz(raCalc, dec, timeJD, location)
            if alt.degrees > 0:
                self.addBuildP((alt.degrees, az.degrees % 360))

        return True

    def generateGoldenSpiral(self, numberPoints):
        """
        based on the evaluations and implementation of CR Drost from 17-05-24 found at:
        https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere
        the implementation of an equally distributed points cloud over on half of the
        hemisphere.

        :param numberPoints:
        :return: true for test purpose
        """

        self.clearBuildP()

        indices = np.arange(0, numberPoints, dtype=float) + 0.5
        phi = np.arccos(1 - 2 * indices / numberPoints)
        theta = np.pi * (1 + 5**0.5) * indices

        # do not transfer to xyz coordinates
        # x, y, z = np.cos(theta) * np.sin(phi), np.sin(theta) * np.sin(phi), np.cos(phi)
        altitude = 90 - np.degrees(phi)
        azimuth = np.degrees(theta) % 360

        for alt, az in zip(altitude, azimuth):
            # only adding above horizon
            if alt > 0:
                self.addBuildP((alt, az))
        return True
Exemplo n.º 8
0
class AstrometryNET(object):
    """
    the class Astrometry inherits all information and handling of astrometry.net handling

    Keyword definitions could be found under
        https://fits.gsfc.nasa.gov/fits_dictionary.html

        >>> astrometry = AstrometryNET(app=app,
        >>>                         )

    """

    __all__ = ['AstrometryNET',
               'solveNET',
               'abortNET',
               ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, parent=None):
        self.parent = parent
        self.data = parent.data
        self.tempDir = parent.tempDir
        self.readFitsData = parent.readFitsData
        self.getSolutionFromWCS = parent.getSolutionFromWCS

        self.result = {'success': False}
        self.process = None

    def runImage2xy(self, binPath='', tempPath='', fitsPath='', timeout=30):
        """
        runImage2xy extracts a list of stars out of the fits image. there is a timeout of
        3 seconds set to get the process finished

        :param binPath:   full path to image2xy executable
        :param tempPath:  full path to star file
        :param fitsPath:  full path to fits file
        :param timeout:
        :return: success
        """

        runnable = [binPath,
                    '-O',
                    '-o',
                    tempPath,
                    fitsPath]

        timeStart = time.time()
        try:
            self.process = subprocess.Popen(args=runnable,
                                            stdout=subprocess.PIPE,
                                            stderr=subprocess.PIPE,
                                            )
            stdout, stderr = self.process.communicate(timeout=timeout)
        except subprocess.TimeoutExpired as e:
            self.log.critical(e)
            return False
        except Exception as e:
            self.log.critical(f'error: {e} happened')
            return False
        else:
            delta = time.time() - timeStart
            self.log.info(f'image2xy took {delta}s return code: '
                          + str(self.process.returncode)
                          + ' stderr: '
                          + stderr.decode().replace('\n', ' ')
                          + ' stdout: '
                          + stdout.decode().replace('\n', ' ')
                          )

        success = (self.process.returncode == 0)
        return success

    def runSolveField(self, binPath='', configPath='', tempPath='', options='', timeout=30):
        """
        runSolveField solves finally the xy star list and writes the WCS data in a fits
        file format

        :param binPath:   full path to image2xy executable
        :param configPath: full path to astrometry.cfg file
        :param tempPath:  full path to star file
        :param options: additional solver options e.g. ra and dec hint
        :param timeout:
        :return: success
        """

        runnable = [binPath,
                    '--overwrite',
                    '--no-remove-lines',
                    '--no-plots',
                    '--no-verify-uniformize',
                    '--uniformize', '0',
                    '--sort-column', 'FLUX',
                    '--scale-units', 'app',
                    '--crpix-center',
                    '--cpulimit', str(timeout),
                    '--config',
                    configPath,
                    tempPath,
                    ]

        runnable += options

        timeStart = time.time()
        try:
            self.process = subprocess.Popen(args=runnable,
                                            stdout=subprocess.PIPE,
                                            stderr=subprocess.PIPE,
                                            )
            stdout, stderr = self.process.communicate(timeout=timeout)
        except subprocess.TimeoutExpired as e:
            self.log.critical(e)
            return False
        except Exception as e:
            self.log.critical(f'error: {e} happened')
            return False
        else:
            delta = time.time() - timeStart
            self.log.info(f'solve-field took {delta}s return code: '
                          + str(self.process.returncode)
                          + ' stderr: '
                          + stderr.decode().replace('\n', ' ')
                          + ' stdout: '
                          + stdout.decode().replace('\n', ' ')
                          )

        success = (self.process.returncode == 0)

        return success

    @staticmethod
    def getWCSHeader(wcsHDU=None):
        """
        getWCSHeader returns the header part of a fits HDU

        :param wcsHDU: fits file with wcs data
        :return: wcsHeader
        """
        if wcsHDU is None:
            return None

        wcsHeader = wcsHDU[0].header
        return wcsHeader

    def solve(self, solver={}, fitsPath='', raHint=None, decHint=None, scaleHint=None,
              radius=2, timeout=30, updateFits=False):
        """
        Solve uses the astrometry.net solver capabilities. The intention is to use an
        offline solving capability, so we need a installed instance. As we go multi
        platform and we need to focus on MW function, we use the astrometry.net package
        which is distributed with KStars / EKOS. Many thanks to them providing such a
        nice package.
        As we go using astrometry.net we focus on the minimum feature set possible to
        omit many of the installation and wrapping work to be done. So we only support
        solving of FITS files, use no python environment for astrometry.net parts (as we
        could access these via MW directly)

        The base outside ideas of implementation come from astrometry.net itself and the
        astrometry implementation from cloudmakers.eu (another nice package for MAC Astro
        software)

        :param solver: which astrometry implementation to choose
        :param fitsPath:  full path to fits file
        :param raHint:  ra dest to look for solve in J2000
        :param decHint:  dec dest to look for solve in J2000
        :param scaleHint:  scale to look for solve in J2000
        :param radius:  search radius around target coordinates
        :param timeout: time after the subprocess will be killed.
        :param updateFits:  if true update Fits image file with wcsHeader data

        :return: success
        """

        self.process = None
        self.result = {'success': False}

        if not solver:
            return False
        if not os.path.isfile(fitsPath):
            self.result['message'] = 'image missing'
            return False

        tempPath = self.tempDir + '/temp.xy'
        configPath = self.tempDir + '/astrometry.cfg'
        solvedPath = self.tempDir + '/temp.solved'
        wcsPath = self.tempDir + '/temp.wcs'
        binPathImage2xy = solver['programPath'] + '/image2xy'
        binPathSolveField = solver['programPath'] + '/solve-field'

        if os.path.isfile(wcsPath):
            os.remove(wcsPath)

        cfgFile = self.tempDir + '/astrometry.cfg'
        with open(cfgFile, 'w+') as outFile:
            outFile.write('cpulimit 300\n')
            outFile.write(f'add_path {solver["indexPath"]}\n')
            outFile.write('autoindex\n')

        # using sextractor in astrometry.net and  KStars
        """
        with open('default.param', 'w+') as outFile:
            outFile.write('MAG_AUTO  Kron-like elliptical aperture magnitude    [mag]\n')
            outFile.write('X_IMAGE   Object position along x                    [pixel]\n')
            outFile.write('Y_IMAGE   Object position along y                    [pixel]\n')

        with open('default.conv', 'w+') as outFile:
            outFile.write('CONV NORM\n')
            outFile.write('1 2 1\n')
            outFile.write('2 4 2\n')
            outFile.write('1 2 1\nn')
        """
        suc = self.runImage2xy(binPath=binPathImage2xy,
                               tempPath=tempPath,
                               fitsPath=fitsPath,
                               timeout=timeout,
                               )
        if not suc:
            self.log.error(f'image2xy error in [{fitsPath}]')
            self.result['message'] = 'image2xy failed'
            return False

        raFITS, decFITS, scaleFITS, _, _ = self.readFitsData(fitsPath=fitsPath)

        # if parameters are passed, they have priority
        if raHint is None:
            raHint = raFITS
        if decHint is None:
            decHint = decFITS
        if scaleHint is None:
            scaleHint = scaleFITS

        searchRatio = 1.1
        ra = transform.convertToHMS(raHint)
        dec = transform.convertToDMS(decHint)
        scaleLow = scaleHint / searchRatio
        scaleHigh = scaleHint * searchRatio
        options = ['--scale-low',
                   f'{scaleLow}',
                   '--scale-high',
                   f'{scaleHigh}',
                   '--ra',
                   f'{ra}',
                   '--dec',
                   f'{dec}',
                   '--radius',
                   f'{radius:1.1f}',
                   ]

        # split between ekos and cloudmakers as cloudmakers use an older version of
        # solve-field, which need the option '--no-fits2fits', whereas the actual
        # version used in KStars throws an error using this option.
        if 'Astrometry.app' in solver['programPath']:
            options.append('--no-fits2fits')

        suc = self.runSolveField(binPath=binPathSolveField,
                                 configPath=configPath,
                                 tempPath=tempPath,
                                 options=options,
                                 timeout=timeout,
                                 )
        if not suc:
            self.log.error(f'solve-field error in [{fitsPath}]')
            self.result['message'] = 'solve-field error'
            return False

        if not os.path.isfile(solvedPath):
            self.log.info(f'solve files for [{fitsPath}] missing')
            self.result['message'] = 'solve failed'
            return False

        if not os.path.isfile(wcsPath):
            self.log.info(f'solve files for [{wcsPath}] missing')
            self.result['message'] = 'solve failed'
            return False

        with fits.open(wcsPath) as wcsHDU:
            wcsHeader = self.getWCSHeader(wcsHDU=wcsHDU)

        with fits.open(fitsPath, mode='update') as fitsHDU:
            solve, header = self.getSolutionFromWCS(fitsHeader=fitsHDU[0].header,
                                                    wcsHeader=wcsHeader,
                                                    updateFits=updateFits)
            fitsHDU[0].header = header

        self.result = {
            'success': True,
            'solvedPath': fitsPath,
            'message': 'Solved',
        }
        self.result.update(solve)

        return True

    def abort(self):
        """
        abortNET stops the solving function hardly just by killing the process

        :return: success
        """

        if self.process:
            self.process.kill()
            return True
        else:
            return False
Exemplo n.º 9
0
class Remote(PyQt5.QtCore.QObject):
    """
    The class Remote inherits all information and handling of remotely controlling
    mountwizzard 4.

        >>> remote = Remote(app=None)

    """

    __all__ = [
        'Remote',
        'startCommunication',
        'stopCommunication',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(
        self,
        app=None,
    ):
        super().__init__()

        self.app = app

        # minimum set for driver package built in
        self.framework = None
        self.run = {'built-in': self}
        self.name = ''

        self.clientConnection = None
        self.tcpServer = None

    def startCommunication(self):
        """
        startCommunication prepares the remote listening by starting a tcp server listening
        on localhost and port 3490.

        :return: success
        """

        if self.tcpServer is not None:
            return False

        self.tcpServer = QtNetwork.QTcpServer(self)
        hostAddress = QtNetwork.QHostAddress('127.0.0.1')

        if not self.tcpServer.listen(hostAddress, 3490):
            self.log.warning('Port already in use')
            self.tcpServer = None
            return False
        else:
            self.log.info('Remote access enabled')
            self.tcpServer.newConnection.connect(self.addConnection)
            return True

    def stopCommunication(self):
        """
        stopCommunication kills all connections and stops the tcpServer

        :return: true for test purpose
        """

        if self.clientConnection is not None:
            self.clientConnection.close()

        if self.tcpServer is not None:
            self.tcpServer = None

        return True

    def addConnection(self):
        """
        addConnection allows a new connection for remote access to mw4 only one connection
        is allowed.

        :return: success
        """

        if self.tcpServer is None:
            return False

        self.clientConnection = self.tcpServer.nextPendingConnection()

        if self.clientConnection == 0:
            self.log.error('Cannot establish incoming connection')
            return False

        self.clientConnection.nextBlockSize = 0
        self.clientConnection.readyRead.connect(self.receiveMessage)
        self.clientConnection.disconnected.connect(self.removeConnection)
        self.clientConnection.error.connect(self.handleError)
        connection = self.clientConnection.peerAddress().toString()
        self.log.info(f'Connection to MountWizzard from {connection}')

        return True

    def receiveMessage(self):
        """
        receiveMessage is the command dispatcher for remote access

        :return: success
        """

        if self.clientConnection.bytesAvailable() == 0:
            return False

        validCommands = [
            'shutdown',
            'shutdown mount',
            'boot mount',
        ]

        connection = self.clientConnection.peerAddress().toString()
        command = str(self.clientConnection.read(100), "ascii")
        command = command.replace('\n', '')
        command = command.replace('\r', '')

        self.log.info(f'Command {command} from {connection} received')

        if command in validCommands:
            self.app.remoteCommand.emit(command)
        else:
            self.log.error(
                f'Unknown command {command} from {connection} received')

        return True

    def removeConnection(self):
        """
        removeConnection clear the existing connection

        :return: true for test purpose
        """

        connection = self.clientConnection.peerAddress().toString()
        self.clientConnection.close()
        self.log.info(f'Connection from {connection} closed')

        return True

    def handleError(self, socketError):
        """
        handleError does error handling -> writing to log

        :param socketError:
        :return: true for test purpose
        """

        connection = self.clientConnection.peerAddress().toString()
        self.log.critical(
            f'Connection from {connection} failed, error: {socketError}')

        return True
Exemplo n.º 10
0
class MountWizzard4(PyQt5.QtCore.QObject):
    """
    MountWizzard4 class is the main class for the application. it loads all windows and
    classes needed to fulfil the work of mountwizzard. any gui work should be handled
    through the window classes. main class is for setup, config, start, persist and
    shutdown the application.
    """

    __all__ = [
        'MountWizzard4',
    ]
    __version__ = version('mountwizzard4')

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # central message and logging dispatching
    message = PyQt5.QtCore.pyqtSignal(str, int)
    messageQueue = Queue()
    redrawHemisphere = PyQt5.QtCore.pyqtSignal()
    remoteCommand = PyQt5.QtCore.pyqtSignal(str)

    # all cyclic tasks
    update0_1s = PyQt5.QtCore.pyqtSignal()
    update1s = PyQt5.QtCore.pyqtSignal()
    update3s = PyQt5.QtCore.pyqtSignal()
    update10s = PyQt5.QtCore.pyqtSignal()
    update60s = PyQt5.QtCore.pyqtSignal()
    update3m = PyQt5.QtCore.pyqtSignal()
    update10m = PyQt5.QtCore.pyqtSignal()
    update30m = PyQt5.QtCore.pyqtSignal()
    update1h = PyQt5.QtCore.pyqtSignal()

    def __init__(
        self,
        mwGlob=None,
    ):
        super().__init__()
        self.expireData = False
        self.mountUp = False
        self.mwGlob = mwGlob
        self.timerCounter = 0
        self.mainW = None
        self.threadPool = PyQt5.QtCore.QThreadPool()
        self.threadPool.setMaxThreadCount(20)
        self.message.connect(self.writeMessageQueue)

        # persistence management through dict
        self.config = {}
        self.loadConfig()

        # write basic data to message window
        profile = self.config.get('profileName', '-')
        self.messageQueue.put(('MountWizzard4 started', 1))
        self.messageQueue.put((f'Workdir is: [{self.mwGlob["workDir"]}]', 1))
        self.messageQueue.put((f'Profile [{profile}] loaded', 1))

        # initialize commands to mount
        pathToData = self.mwGlob['dataDir']
        self.mount = qtmount.Mount(
            host='localhost',
            MAC='00.c0.08.87.35.db',
            threadPool=self.threadPool,
            pathToData=pathToData,
            expire=False,
            verbose=False,
        )

        # setting location to last know config
        topo = self.initConfig()
        self.mount.obsSite.location = topo
        self.mount.signals.mountUp.connect(self.loadMountData)

        # get all planets for calculation
        try:
            self.planets = self.mount.obsSite.loader('de421_23.bsp')
        except Exception as e:
            self.log.critical(f'Failed loading planets: {e}')
            self.planets = None

        self.relay = KMRelay(host='localhost')
        self.sensorWeather = SensorWeather(self)
        self.onlineWeather = OnlineWeather(self)
        self.directWeather = DirectWeather(self)
        self.cover = FlipFlat(self)
        self.dome = Dome(self)
        self.camera = Camera(self)
        self.filter = Filter(self)
        self.focuser = Focuser(self)
        self.telescope = Telescope(self)
        self.skymeter = Skymeter(self)
        self.power = PegasusUPB(self)
        self.data = DataPoint(self, configDir=self.mwGlob['configDir'])
        self.hipparcos = Hipparcos(self)
        self.measure = MeasureData(self)
        self.remote = Remote(self)
        self.astrometry = Astrometry(self, tempDir=mwGlob['tempDir'])

        # get the window widgets up
        self.mainW = MainWindow(self)

        # link cross widget gui signals as all ui widgets have to be present
        self.uiWindows = {
            'showMessageW': {
                'button': self.mainW.ui.openMessageW,
                'classObj': None,
                'name': 'MessageDialog',
                'class': MessageWindow,
            },
            'showHemisphereW': {
                'button': self.mainW.ui.openHemisphereW,
                'classObj': None,
                'name': 'HemisphereDialog',
                'class': HemisphereWindow,
            },
            'showImageW': {
                'button': self.mainW.ui.openImageW,
                'classObj': None,
                'name': 'ImageDialog',
                'class': ImageWindow,
            },
            'showMeasureW': {
                'button': self.mainW.ui.openMeasureW,
                'classObj': None,
                'name': 'MeasureDialog',
                'class': MeasureWindow,
            },
            'showSatelliteW': {
                'button': self.mainW.ui.openSatelliteW,
                'classObj': None,
                'name': 'SatelliteDialog',
                'class': SatelliteWindow,
            },
        }
        # todo: we can only add keypad on arm when we have compiled version
        if platform.machine() != 'armv7l':
            self.uiWindows['showKeypadW'] = {
                'button': self.mainW.ui.openKeypadW,
                'classObj': None,
                'name': 'KeypadDialog',
                'class': KeypadWindow,
            }

        # show all sub windows
        self.showWindows()

        # connecting buttons to window open close
        for win in self.uiWindows:
            self.uiWindows[win]['button'].clicked.connect(self.toggleWindow)

        # starting mount communication
        self.mount.startTimers()

        self.timer0_1s = PyQt5.QtCore.QTimer()
        self.timer0_1s.setSingleShot(False)
        self.timer0_1s.timeout.connect(self.sendUpdate)
        self.timer0_1s.start(100)

        # finishing for test: MW4 runs with keyword 'test' for 10 seconds an terminates
        if not hasattr(sys, 'argv'):
            return
        if not len(sys.argv) > 1:
            return
        if sys.argv[1] == 'test':
            self.update10s.connect(self.quitSave)

    def toggleWindow(self, windowTag=''):
        """
        togglePowerPort  toggles the state of the power switch
        :return: true for test purpose
        """

        for win in self.uiWindows:
            isSender = (self.uiWindows[win]['button'] == self.sender())
            isWindowTag = (win == windowTag)

            if not isSender and not isWindowTag:
                continue

            winObj = self.uiWindows[win]
            if not winObj['classObj']:
                newWindow = winObj['class'](self)
                # make new object instance from window
                winObj['classObj'] = newWindow
                winObj['classObj'].destroyed.connect(self.deleteWindow)

            else:
                winObj['classObj'].close()

        return True

    def deleteWindow(self, widget):
        """

        :return: success
        """

        if not widget:
            return False

        for win in self.uiWindows:
            winObj = self.uiWindows[win]

            if winObj['name'] != widget.objectName():
                continue

            winObj['classObj'] = None
            gc.collect()

        return True

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return:
        """

        # set observer position to last one first, to greenwich if not known
        lat = self.config.get('topoLat', 51.47)
        lon = self.config.get('topoLon', 0)
        elev = self.config.get('topoElev', 46)
        topo = skyfield.api.Topos(longitude_degrees=lon,
                                  latitude_degrees=lat,
                                  elevation_m=elev)

        config = self.config.get('mainW', {})
        if config.get('loglevelDeepDebug', True):
            level = 'DEBUG'
        elif config.get('loglevelDebug', True):
            level = 'INFO'
        else:
            level = 'WARN'
        setCustomLoggingLevel(level)

        return topo

    def storeConfig(self):
        """
        storeConfig collects all persistent data from mainApp and it's submodules and stores
        it in the persistence dictionary for later saving

        :return: success for test purpose
        """

        config = self.config = {}
        location = self.mount.obsSite.location
        if location is not None:
            config['topoLat'] = location.latitude.degrees
            config['topoLon'] = location.longitude.degrees
            config['topoElev'] = location.elevation.m
        self.mainW.storeConfig()

        for win in self.uiWindows:
            winObj = self.uiWindows[win]
            config[win] = bool(winObj['classObj'])
            if config[win]:
                winObj['classObj'].storeConfig()

        return True

    def showWindows(self):
        """

        :return: true for test purpose
        """

        for win in self.uiWindows:
            if self.config.get(win, False):
                self.toggleWindow(windowTag=win)

        return True

    def sendUpdate(self):
        """
        sendUpdate send regular signals in 1 and 10 seconds to enable regular tasks.
        it tries to avoid sending the signals at the same time.

        :return: true for test purpose
        """

        self.timerCounter += 1
        if self.timerCounter % 1 == 0:
            self.update0_1s.emit()
        if (self.timerCounter + 5) % 10 == 0:
            self.update1s.emit()
        if (self.timerCounter + 10) % 30 == 0:
            self.update3s.emit()
        if (self.timerCounter + 20) % 100 == 0:
            self.update10s.emit()
        if (self.timerCounter + 25) % 600 == 0:
            self.update60s.emit()
        if (self.timerCounter + 12) % 1800 == 0:
            self.update3m.emit()
        if (self.timerCounter + 13) % 6000 == 0:
            self.update10m.emit()
        if (self.timerCounter + 14) % 18000 == 0:
            self.update30m.emit()
        if (self.timerCounter + 15) % 36000 == 0:
            self.update1h.emit()
        return True

    def quit(self):
        """
        quit without saving persistence data

        :return:    True for test purpose
        """

        self.mount.stopTimers()
        self.measure.timerTask.stop()
        self.relay.timerTask.stop()
        self.timer0_1s.stop()
        self.message.emit('MountWizzard4 manual stopped with quit', 1)
        PyQt5.QtCore.QCoreApplication.quit()
        return True

    def quitSave(self):
        """
        quit with saving persistence data

        :return:    True for test purpose
        """

        self.mount.stopTimers()
        self.measure.timerTask.stop()
        self.relay.timerTask.stop()
        self.storeConfig()
        self.saveConfig()
        self.timer0_1s.stop()
        self.message.emit('MountWizzard4 manual stopped with quit/save', 1)
        PyQt5.QtCore.QCoreApplication.quit()
        return True

    @staticmethod
    def defaultConfig(config=None):
        """

        :param config:
        :return:
        """

        if config is None:
            config = dict()
        config['profileName'] = 'config'
        config['version'] = '4.0'
        return config

    def loadConfig(self, name=None):
        """
        loadConfig loads a json file from disk and stores it to the config dicts for
        persistent data. if a file path is given, that's the relevant file, otherwise
        loadConfig loads from th default file, which is config.cfg

        :param      name:   name of the config file
        :return:    success if file could be loaded
        """

        configDir = self.mwGlob['configDir']
        # looking for file existence and creating new if necessary

        if name is None:
            name = 'config'
        fileName = configDir + '/' + name + '.cfg'

        if not os.path.isfile(fileName):
            self.config = self.defaultConfig()
            if name == 'config':
                self.log.warning(
                    'Config file {0} not existing'.format(fileName))
                return True
            else:
                return False

        # parsing the default file
        try:
            with open(fileName, 'r') as configFile:
                configData = json.load(configFile)
        except Exception as e:
            self.log.critical('Cannot parse: {0}, error: {1}'.format(
                fileName, e))
            self.config = self.defaultConfig()
            return False

        # check if reference ist still to default -> correcting
        if configData.get('reference', '') == 'config':
            del configData['reference']
        elif not configData.get('reference', ''):
            configData['profileName'] = 'config'

        # loading default and finishing up
        if configData['profileName'] == 'config':
            self.config = self.convertData(configData)
            return True

        # checking if reference to another file is available
        refName = configData.get('reference', 'config')
        if refName != name:
            suc = self.loadConfig(refName)
        else:
            self.config = configData
            return True
        return suc

    @staticmethod
    def convertData(data):
        """
        convertDate tries to convert data from an older or newer version of the config
        file to the actual needed one.

        :param      data: config data as dict
        :return:    data: config data as dict
        """

        return data

    def saveConfig(self, name=None):
        """
        saveConfig saves a json file to disk from the config dicts for
        persistent data.

        :param      name:   name of the config file
        :return:    success
        """

        configDir = self.mwGlob['configDir']

        if self.config.get('profileName', '') == 'config':
            if 'reference' in self.config:
                del self.config['reference']

        # default saving for reference
        if name is None:
            name = self.config.get('reference', 'config')

        fileName = configDir + '/' + name + '.cfg'
        with open(fileName, 'w') as outfile:
            json.dump(self.config, outfile, sort_keys=True, indent=4)
        # if we save a reference first, we have to save the config as well
        if name != 'config':
            fileName = configDir + '/config.cfg'
            with open(fileName, 'w') as outfile:
                json.dump(self.config, outfile, sort_keys=True, indent=4)
        return True

    def loadMountData(self, status):
        """
        loadMountData polls data from mount if connected otherwise clears all entries
        in attributes.

        :param      status: connection status to mount computer
        :return:    status how it was called
        """

        if status and not self.mountUp:
            self.mount.getFW()
            self.mount.getLocation()
            self.mount.cycleSetting()
            self.mainW.refreshName()
            self.mainW.refreshModel()
            self.mount.getTLE()
            self.mountUp = True
            return True
        elif not status and self.mountUp:
            location = self.mount.obsSite.location
            self.mount.resetData()
            self.mount.obsSite.location = location
            self.mountUp = False
            return False

        return status

    def writeMessageQueue(self, message, mType):
        """
        writeMessageQueue receives all signals handling the message sending and puts them in
        a queue. the queue enables the print of messages event when the message window is not
        open.

        :param message:
        :param mType:
        :return: True for test purpose
        """

        self.messageQueue.put((message, mType))

        return True
Exemplo n.º 11
0
class IndiClass(object):
    """
    the class indiClass inherits all information and handling of indi devices
    this class will be only referenced from other classes and not directly used

        >>> indi = IndiClass(app=None, data={})
    """

    __all__ = ['IndiClass']

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    RETRY_DELAY = 1500
    NUMBER_RETRY = 5

    def __init__(self, app=None, data={}):
        super().__init__()

        self.app = app

        self.client = qtIndiBase.Client(host=None)
        self.name = ''
        self._host = ('localhost', 7624)
        self.data = data

        self.retryCounter = 0
        self.device = None
        self.showMessages = False

        self.timerRetry = PyQt5.QtCore.QTimer()
        self.timerRetry.setSingleShot(True)
        self.timerRetry.timeout.connect(self.startRetry)

        # link signals
        self.client.signals.newDevice.connect(self.newDevice)
        self.client.signals.removeDevice.connect(self.removeDevice)
        self.client.signals.newProperty.connect(self.connectDevice)
        self.client.signals.newNumber.connect(self.updateNumber)
        self.client.signals.defNumber.connect(self.updateNumber)
        self.client.signals.newSwitch.connect(self.updateSwitch)
        self.client.signals.defSwitch.connect(self.updateSwitch)
        self.client.signals.newText.connect(self.updateText)
        self.client.signals.defText.connect(self.updateText)
        self.client.signals.newLight.connect(self.updateLight)
        self.client.signals.defLight.connect(self.updateLight)
        self.client.signals.newBLOB.connect(self.updateBLOB)
        self.client.signals.defBLOB.connect(self.updateBLOB)
        self.client.signals.deviceConnected.connect(self.setUpdateConfig)
        self.client.signals.serverConnected.connect(self.serverConnected)
        self.client.signals.serverDisconnected.connect(self.serverDisconnected)
        self.client.signals.newMessage.connect(self.updateMessage)

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, value):
        self._host = value
        self.client.host = value

    def serverConnected(self):
        """
        serverConnected is called when the server signals the connection. if so, we would
        like to start watching the defined device. this will be triggered directly

        :return: success
        """

        if self.name:
            suc = self.client.watchDevice(self.name)
            self.log.info(f'Indi watch: {self.name}, watch: result:{suc}')
            return suc
        return False

    @staticmethod
    def serverDisconnected(devices):
        """

        :param devices:
        :return: true for test purpose
        """

        return True

    def newDevice(self, deviceName):
        """
        newDevice is called whenever a new device entry is received in indi client. it
        adds the device if the name fits to the given name in configuration.

        :param deviceName:
        :return: true for test purpose
        """

        if deviceName == self.name:
            self.device = self.client.getDevice(deviceName)
            self.app.message.emit(f'INDI device found:   [{deviceName}]', 0)
        # else:
        #     self.app.message.emit(f'INDI device snoops:     [{deviceName}]', 0)

        return True

    def removeDevice(self, deviceName):
        """
        removeDevice is called whenever a device is removed from indi client. it sets
        the device entry to None

        :param deviceName:
        :return: true for test purpose
        """

        if deviceName == self.name:
            self.app.message.emit(f'INDI removed device: [{deviceName}]', 0)
            self.device = None
            self.data.clear()
            return True
        else:
            return False

    def startRetry(self):
        """
        startRetry tries to connect the server a NUMBER_RETRY times, if necessary with a
        delay of RETRY_DELAY

        :return: True for test purpose
        """

        if not self.name:
            return False

        self.retryCounter += 1

        if not self.data:
            self.startCommunication()
            self.log.info(
                f'Indi server {self.name} connection retry: {self.retryCounter}'
            )
        else:
            self.retryCounter = 0

        if self.retryCounter < self.NUMBER_RETRY:
            self.timerRetry.start(self.RETRY_DELAY)

        return True

    def startCommunication(self):
        """
        startCommunication adds a device on the watch list of the server.

        :return: success of reconnecting to server
        """

        self.client.startTimers()
        suc = self.client.connectServer()
        if not suc:
            self.log.info(f'Cannot start connection to: {self.name}')
        else:
            # adding a single retry if first connect does not happen
            self.timerRetry.start(self.RETRY_DELAY)
        return suc

    def stopCommunication(self):
        """
        stopCommunication adds a device on the watch list of the server.

        :return: success of reconnecting to server
        """

        self.client.stopTimers()
        suc = self.client.disconnectServer(self.name)
        self.name = ''
        return suc

    def connectDevice(self, deviceName, propertyName):
        """
        connectDevice is called when a new property is received and checks it against
        property CONNECTION. if this is there, we could check the connection state of
        a given device

        :param deviceName:
        :param propertyName:
        :return: success if device could connect
        """

        if propertyName != 'CONNECTION':
            return False

        suc = False
        if deviceName == self.name:
            suc = self.client.connectDevice(deviceName=deviceName)
        return suc

    def setUpdateConfig(self, deviceName):
        """
        _setUpdateRate corrects the update rate of weather devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """
        pass

    def updateNumber(self, deviceName, propertyName):
        """
        updateNumber is called whenever a new number is received in client. it runs
        through the device list and writes the number data to the according locations.

        :param deviceName:
        :param propertyName:
        :return: success
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getNumber(propertyName).items():
            key = propertyName + '.' + element
            self.data[key] = value

            # print(self.name, key, value)

        return True

    def updateSwitch(self, deviceName, propertyName):
        """
        updateSwitch is called whenever a new switch is received in client. it runs
        through the device list and writes the switch data to the according locations.

        :param deviceName:
        :param propertyName:
        :return: success
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getSwitch(propertyName).items():
            key = propertyName + '.' + element
            self.data[key] = value

            # print(self.name, key, value)

        return True

    def updateText(self, deviceName, propertyName):
        """
        updateText is called whenever a new text is received in client. it runs
        through the device list and writes the text data to the according locations.

        :param deviceName:
        :param propertyName:
        :return: success
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getText(propertyName).items():
            key = propertyName + '.' + element
            self.data[key] = value

            # print(self.name, key, value)

        return True

    def updateLight(self, deviceName, propertyName):
        """
        updateLight is called whenever a new light is received in client. it runs
        through the device list and writes the light data to the according locations.

        :param deviceName:
        :param propertyName:
        :return: success
        """
        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getLight(propertyName).items():
            key = propertyName + '.' + element
            self.data[key] = value

            # print(self.name, key, value)

        return True

    def updateBLOB(self, deviceName, propertyName):
        """
        updateBLOB is called whenever a new BLOB is received in client. it runs
        through the device list and writes the BLOB data to the according locations.

        :param deviceName:
        :param propertyName:
        :return: success
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        return True

    @staticmethod
    def removePrefix(text, prefix):
        """

        :param text:
        :param prefix:
        :return:
        """

        value = text[text.startswith(prefix) and len(prefix):]
        value = value.strip()
        return value

    def updateMessage(self, device, text):
        """
        message take a message send by indi device and emits them in the user message
        window as well.

        :param device: device name
        :param text: message received
        :return: success
        """

        if self.showMessages:
            if text.startswith('[WARNING]'):
                text = self.removePrefix(text, '[WARNING]')
                self.app.message.emit(device + ' -> ' + text, 0)
            elif text.startswith('[ERROR]'):
                text = self.removePrefix(text, '[ERROR]')
                self.app.message.emit(device + ' -> ' + text, 2)
            else:
                self.app.message.emit(device + ' -> ' + text, 0)
            return True
        return False
Exemplo n.º 12
0
class KMRelay(PyQt5.QtCore.QObject):
    """
    The class KMRelay inherits all information and handling of KMtronic relay board
    attributes of the connected board and provides the abstracted interface.

        >>> relay = KMRelay(host=None, user='', password='')

    """

    __all__ = [
        'KMRelay',
        'startCommunication',
        'stopCommunication',
        'cyclePolling',
        'pulse',
        'switch',
        'set',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # polling cycle for relay box
    CYCLE_POLLING = 1000
    # default port for KMTronic Relay
    DEFAULT_PORT = 80
    # timeout for requests
    TIMEOUT = 0.5
    # width for pulse
    PULSEWIDTH = 0.5
    # signal if correct status received and decoded
    statusReady = PyQt5.QtCore.pyqtSignal()

    def __init__(
        self,
        host=None,
        user=None,
        password=None,
    ):
        super().__init__()

        # minimum set for driver package built in
        self.framework = None
        self.run = {'built-in': self}
        self.name = ''

        self.host = host
        self.mutexPoll = PyQt5.QtCore.QMutex()
        self.user = user
        self.password = password
        self.status = [0] * 8

        self.timerTask = PyQt5.QtCore.QTimer()
        self.timerTask.setSingleShot(False)
        self.timerTask.timeout.connect(self.cyclePolling)

    @property
    def host(self):
        return self._host

    def checkFormat(self, value):
        # checking format
        if not value:
            self.log.info('Host value not configured')
            return None
        if not isinstance(value, (tuple, str)):
            self.log.warning(f'Wrong host value: {value}')
            return None
        # now we got the right format
        if isinstance(value, str):
            value = (value, self.DEFAULT_PORT)
        return value

    @host.setter
    def host(self, value):
        value = self.checkFormat(value)
        self._host = value

    @property
    def user(self):
        return self._user

    @user.setter
    def user(self, value):
        self._user = value

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, value):
        self._password = value

    def startCommunication(self):
        """
        startCommunication enables the cyclic timers for polling necessary relay data.

        :return: success
        """

        if self._host is None:
            return False
        if not self._host[0]:
            return False
        if not self._host[1]:
            return False

        self.timerTask.start(self.CYCLE_POLLING)

        return True

    def stopCommunication(self):
        """
        stopCommunication disables the cyclic timers for polling necessary relay data.

        :return: True for test purpose
        """
        self.timerTask.stop()
        return True

    def debugOutput(self, result=None):
        """
        debugOutput writes a nicely formed output for diagnosis in relay environment

        :param result: value from requests get commend
        :return: True for test purpose
        """

        if result is None:
            self.log.warning('No valid result')
            return False

        text = result.text.replace('\r\n', ', ')
        reason = result.reason
        status = result.status_code
        url = result.url
        elapsed = result.elapsed

        self.log.debug(f'Result: {url}, {reason}, {status}, {elapsed}, {text}')

        return True

    def getRelay(self, url='/status.xml', debug=True):
        """
        getRelay sets and reads data from the given host ip using the given
        user and password

        :param url: web address of relay box
        :param debug: write extended debug output
        :return: result: return values from web interface of box
        """

        if self.host is None:
            return None

        if not self.mutexPoll.tryLock():
            return None

        auth = requests.auth.HTTPBasicAuth(
            self.user,
            self.password,
        )
        url = f'http://{self._host[0]}:{self._host[1]}{url}'
        result = None

        try:
            result = requests.get(url, auth=auth, timeout=self.TIMEOUT)
        except requests.exceptions.Timeout:
            self.log.info(f'Connection timeout: [{url}]')
        except requests.exceptions.ConnectionError:
            self.log.info(f'Connection error: [{url}]')
        except Exception as e:
            self.log.critical(f'Error in request: {e}')

        if debug:
            self.debugOutput(result=result)

        self.mutexPoll.unlock()
        return result

    def cyclePolling(self):
        """
        cyclePolling reads the status of the relay status of each single relay.
        with success the statusReady single is sent.

        :return: success
        """

        value = self.getRelay('/status.xml', debug=False)

        if value is None:
            return False
        if value.reason != 'OK':
            return False

        lines = value.text.splitlines()
        for line in lines:
            value = re.findall(r'\d', line)
            if not value:
                continue
            value = [int(s) for s in value]
            self.status[value[0] - 1] = value[1]

        self.statusReady.emit()
        return True

    def getByte(self, relayNumber=0, state=False):
        """
        getByte generates the right bit mask for setting or resetting the relay mask

        :param relayNumber: relay number
        :param state: state to archive
        :return: bit mask for switching
        """

        byteStat = 0b0
        for i, status in enumerate(self.status):
            if status:
                byteStat = byteStat | 1 << i
        position = 1 << relayNumber
        byteOn = byteStat | position
        byteOff = byteOn & ~position

        if state:
            return byteOn
        else:
            return byteOff

    def pulse(self, relayNumber):
        """
        pulse switches a relay on for one second and off back.

        :param relayNumber: number of relay to be pulsed, counting from 0 onwards
        :return: success
        """

        self.log.info(f'Pulse relay:{relayNumber}')
        byteOn = self.getByte(relayNumber=relayNumber, state=True)
        byteOff = self.getByte(relayNumber=relayNumber, state=False)
        value1 = self.getRelay(f'/FFE0{byteOn:02X}')
        time.sleep(self.PULSEWIDTH)
        value2 = self.getRelay(f'/FFE0{byteOff:02X}')

        if value1 is None or value2 is None:
            self.log.error(f'Relay:{relayNumber}')
            return False
        elif value1.reason != 'OK' or value2.reason != 'OK':
            self.log.error(f'Relay:{relayNumber}')
            return False

        return True

    def switch(self, relayNumber):
        """
        switch toggles the relay status (on, off)

        :param relayNumber: number of relay to be pulsed, counting from 0 onwards
        :return: success
        """

        self.log.info(f'Switch relay:{relayNumber}')
        value = self.getRelay('/relays.cgi?relay={0:1d}'.format(relayNumber +
                                                                1))

        if value is None:
            self.log.error(f'Relay:{relayNumber}')
            return False
        elif value.reason != 'OK':
            self.log.error(f'Relay:{relayNumber}')
            return False

        return True

    def set(self, relayNumber, value):
        """
        set toggles the relay status to the desired value (on, off)

        :param relayNumber: number of relay to be pulsed, counting from 0 onwards
        :param value: relay state.
        :return: success
        """

        self.log.info(f'Set relay:{relayNumber}')
        byteOn = self.getByte(relayNumber=relayNumber, state=value)
        value = self.getRelay(f'/FFE0{byteOn:02X}')

        if value is None:
            self.log.error(f'Relay:{relayNumber}')
            return False
        elif value.reason != 'OK':
            self.log.error(f'Relay:{relayNumber}')
            return False

        return True
Exemplo n.º 13
0
class MessageWindow(widget.MWidget):
    """
    the message window class handles

    """

    __all__ = [
        'MessageWindow',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):
        super().__init__()
        self.app = app
        self.ui = message_ui.Ui_MessageDialog()
        self.ui.setupUi(self)
        self.initUI()

        self.messColor = [
            self.COLOR_ASTRO,
            self.COLOR_WHITE,
            self.COLOR_YELLOW,
            self.COLOR_RED,
        ]
        self.messFont = [
            PyQt5.QtGui.QFont.Normal,
            PyQt5.QtGui.QFont.Bold,
            PyQt5.QtGui.QFont.Normal,
            PyQt5.QtGui.QFont.Normal,
        ]

        self.initConfig()
        self.showWindow()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return: True for test purpose
        """

        if 'messageW' not in self.app.config:
            self.app.config['messageW'] = {}
        config = self.app.config['messageW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 600)
        self.resize(800, height)
        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'messageW' not in self.app.config:
            self.app.config['messageW'] = {}
        config = self.app.config['messageW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()

        return True

    def closeEvent(self, closeEvent):
        self.storeConfig()

        # gui signals
        self.ui.clear.clicked.disconnect(self.clearWindow)
        # self.app.message.disconnect(self.writeMessage)
        self.app.update1s.disconnect(self.writeMessage)

        super().closeEvent(closeEvent)

    def showWindow(self):
        self.show()

        # gui signals
        self.ui.clear.clicked.connect(self.clearWindow)
        # self.app.message.connect(self.writeMessage)
        self.app.update1s.connect(self.writeMessage)

    def clearWindow(self):
        """
        clearWindow resets the window and shows empty text.

        :return: true for test purpose
        """

        self.ui.message.clear()
        return True

    def writeMessage(self):
        """
        writeMessage takes singles with message and writes them to the text browser window.
        types:
            0: normal text
            1: highlighted text
            2: warning text
            3: error text

        :return: true for test purpose
        """

        while not self.app.messageQueue.empty():
            message, mType = self.app.messageQueue.get()

            if mType < 0:
                continue
            if mType > len(self.messColor):
                continue
            prefix = time.strftime('%H:%M:%S ', time.localtime())
            message = prefix + message
            self.log.info('Message window: [{0}]'.format(message))
            self.ui.message.setTextColor(self.messColor[mType])
            self.ui.message.setFontWeight(self.messFont[mType])
            self.ui.message.insertPlainText(message + '\n')
            self.ui.message.moveCursor(PyQt5.QtGui.QTextCursor.End)
        return True
Exemplo n.º 14
0
class DevicePopup(PyQt5.QtWidgets.QDialog, widget.MWidget):
    """
    the DevicePopup window class handles

    """

    __all__ = [
        'DevicePopup',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # INDI device types
    indiTypes = {
        'telescope': (1 << 0),
        'camera': (1 << 1),
        'guider': (1 << 2),
        'focuser': (1 << 3),
        'filter': (1 << 4),
        'dome': (1 << 5),
        'weather': (1 << 7),
        'skymeter': 0,
        'cover': (1 << 9) | (1 << 10),
        'power': (1 << 7) | (1 << 3)
    }

    indiDefaults = {
        'telescope': 'LX200 10micron',
        'skymeter': 'SQM',
        'power': 'Pegasus UPB',
    }

    def __init__(self,
                 geometry=None,
                 driver='',
                 deviceType='',
                 framework={},
                 data=None):

        super().__init__()
        self.ui = Ui_DevicePopup()
        self.ui.setupUi(self)
        self.initUI()
        self.setWindowModality(PyQt5.QtCore.Qt.ApplicationModal)
        self.data = data
        self.driver = driver
        self.deviceType = deviceType
        self.framework = framework
        self.indiClass = None
        self.indiSearchNameList = ()
        self.indiSearchType = None
        self.returnValues = {'close': 'cancel'}

        # setting to center of parent image
        x = geometry[0] + (geometry[2] - self.width()) / 2
        y = geometry[1] + (geometry[3] - self.height()) / 2
        self.move(x, y)

        self.ui.cancel.clicked.connect(self.close)
        self.ui.ok.clicked.connect(self.storeConfig)
        self.ui.indiSearch.clicked.connect(self.searchDevices)
        self.ui.copyAlpaca.clicked.connect(self.copyAllAlpacaSettings)
        self.ui.copyIndi.clicked.connect(self.copyAllIndiSettings)
        self.initConfig()

    def initConfig(self):
        """

        :return: True for test purpose
        """

        # populate data
        deviceData = self.data.get(self.driver, {})
        selectedFramework = deviceData.get('framework', 'indi')
        self.indiSearchType = self.indiTypes.get(self.deviceType, 0xff)
        self.setWindowTitle(f'Setup for: {self.driver}')

        # populating indi data
        self.ui.indiHost.setText(deviceData.get('indiHost', 'localhost'))
        self.ui.indiPort.setText(deviceData.get('indiPort', '7624'))
        self.ui.indiNameList.clear()
        self.ui.indiNameList.setView(PyQt5.QtWidgets.QListView())
        indiName = deviceData.get('indiName', '')
        nameList = deviceData.get('indiNameList', [])
        if not nameList:
            self.ui.indiNameList.addItem('-')
        for i, name in enumerate(nameList):
            self.ui.indiNameList.addItem(name)
            if indiName == name:
                self.ui.indiNameList.setCurrentIndex(i)
        self.ui.indiMessages.setChecked(deviceData.get('indiMessages', False))
        self.ui.indiLoadConfig.setChecked(
            deviceData.get('indiLoadConfig', False))

        # populating alpaca data
        self.ui.alpacaProtocol.setCurrentIndex(
            deviceData.get('alpacaProtocol', 0))
        self.ui.alpacaHost.setText(deviceData.get('alpacaHost', 'localhost'))
        self.ui.alpacaPort.setText(deviceData.get('alpacaPort', '11111'))
        number = int(deviceData.get('alpacaName', '"":0').split(':')[1])
        self.ui.alpacaNumber.setValue(number)
        self.ui.alpacaUser.setText(deviceData.get('alpacaUser', 'user'))
        self.ui.alpacaPassword.setText(
            deviceData.get('alpacaPassword', 'password'))

        # for fw in self.framework:
        tabWidget = self.ui.tab.findChild(PyQt5.QtWidgets.QWidget,
                                          selectedFramework)
        tabIndex = self.ui.tab.indexOf(tabWidget)
        self.ui.tab.setCurrentIndex(tabIndex)

        for index in range(0, self.ui.tab.count()):
            if self.ui.tab.tabText(index).lower() in self.framework:
                continue
            self.ui.tab.setTabEnabled(index, False)
        self.show()

        return True

    def storeConfig(self):
        """

        :return: true for test purpose
        """
        # collecting indi data
        self.data[self.driver]['indiHost'] = self.ui.indiHost.text()
        self.data[self.driver]['indiPort'] = self.ui.indiPort.text()
        self.data[self.driver]['indiName'] = self.ui.indiNameList.currentText()

        model = self.ui.indiNameList.model()
        nameList = []
        for index in range(model.rowCount()):
            nameList.append(model.item(index).text())
        self.data[self.driver]['indiNameList'] = nameList
        self.data[
            self.driver]['indiMessages'] = self.ui.indiMessages.isChecked()
        self.data[self.driver][
            'indiLoadConfig'] = self.ui.indiLoadConfig.isChecked()

        # collecting alpaca data
        self.data[self.driver][
            'alpacaProtocol'] = self.ui.alpacaProtocol.currentIndex()
        self.data[self.driver]['alpacaHost'] = self.ui.alpacaHost.text()
        self.data[self.driver]['alpacaPort'] = self.ui.alpacaPort.text()
        name = f'{self.deviceType}:{self.ui.alpacaNumber.value()}'
        self.data[self.driver]['alpacaName'] = name
        self.data[self.driver]['alpacaUser'] = self.ui.alpacaUser.text()
        self.data[
            self.driver]['alpacaPassword'] = self.ui.alpacaPassword.text()

        # storing ok as closing
        self.returnValues['close'] = 'ok'

        # finally closing window
        self.close()

        return True

    def closeEvent(self, event):
        """
        closeEvent collects all data necessary for the following process

        :param event:
        :return:
        """
        # getting last setting:
        self.returnValues['framework'] = self.ui.tab.tabText(
            self.ui.tab.currentIndex()).lower()
        super().closeEvent(event)

        return

    def copyAllIndiSettings(self):
        """
        copyAllIndiSettings transfers all data from host, port, messages to all other
        driver settings

        :return: true for test purpose
        """
        for driver in self.data:
            self.data[driver]['indiHost'] = self.ui.indiHost.text()
            self.data[driver]['indiPort'] = self.ui.indiPort.text()
            self.data[driver][
                'indiMessages'] = self.ui.indiLoadConfig.isChecked()
            self.data[driver][
                'indiLoadConfig'] = self.ui.indiLoadConfig.isChecked()

        # memorizing that copy was done:
        self.returnValues['copyIndi'] = True

        return True

    def copyAllAlpacaSettings(self):
        """
        copyAllAlpacaSettings transfers all data from protocol, host, port, user, password to
        all other driver settings

        :return: true for test purpose
        """
        for driver in self.data:
            self.data[driver][
                'alpacaProtocol'] = self.ui.alpacaProtocol.currentIndex()
            self.data[driver]['alpacaHost'] = self.ui.alpacaHost.text()
            self.data[driver]['alpacaPort'] = self.ui.alpacaPort.text()
            self.data[driver]['alpacaNumber'] = self.ui.alpacaNumber.value()
            self.data[driver]['alpacaUser'] = self.ui.alpacaUser.text()
            self.data[driver]['alpacaPassword'] = self.ui.alpacaPassword.text()

        # memorizing that copy was done:
        self.returnValues['copyAlpaca'] = True

        return True

    def addDevicesWithType(self, deviceName, propertyName):
        """
        addDevicesWithType gety called whenever a new device send out text messages. than it
        checks, if the device type fits to the search type desired. if they match, the
        device name is added to the list.

        :param deviceName:
        :param propertyName:
        :return: success
        """

        device = self.indiClass.client.devices.get(deviceName)
        if not device:
            return False

        interface = device.getText(propertyName).get('DRIVER_INTERFACE', None)

        if interface is None:
            return False

        if self.indiSearchType is None:
            return False

        interface = int(interface)

        if interface & self.indiSearchType:
            self.indiSearchNameList.append(deviceName)

        return True

    def searchDevices(self):
        """
        searchDevices implements a search for devices of a certain device type. it is called
        from a button press and checks which button it was. after that for the right device
        it collects all necessary data for host value, instantiates an INDI client and
        watches for all devices connected to this server. Than it connects a subroutine for
        collecting the right device names and opens a model dialog. the data collection
        takes place as long as the model dialog is open. when the user closes this dialog, the
        collected data is written to the drop down list.

        :return:  success finding
        """

        self.indiSearchNameList = list()

        if self.driver in self.indiDefaults:
            self.indiSearchNameList.append(self.indiDefaults[self.driver])

        else:
            host = (self.ui.indiHost.text(), int(self.ui.indiPort.text()))
            self.indiClass = IndiClass()
            self.indiClass.host = host

            self.indiClass.client.signals.defText.connect(
                self.addDevicesWithType)
            self.indiClass.client.connectServer()
            self.indiClass.client.watchDevice()
            msg = PyQt5.QtWidgets.QMessageBox
            msg.information(
                self, 'Searching Devices',
                f'Search for [{self.driver}] could take some seconds!')
            self.indiClass.client.disconnectServer()

        self.ui.indiNameList.clear()
        self.ui.indiNameList.setView(PyQt5.QtWidgets.QListView())

        for name in self.indiSearchNameList:
            self.log.info(f'Indi search found: {name}')

        for deviceName in self.indiSearchNameList:
            self.ui.indiNameList.addItem(deviceName)

        return True
Exemplo n.º 15
0
class MeasureWindow(widget.MWidget):
    """
    the measure window class handles

    """

    __all__ = [
        'MeasureWindow',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    NUMBER_POINTS = 250
    NUMBER_XTICKS = 5

    def __init__(self, app):
        super().__init__()
        self.app = app

        self.ui = measure_ui.Ui_MeasureDialog()
        self.ui.setupUi(self)
        self.initUI()

        self.refreshCounter = 1
        self.measureIndex = 0
        self.timeIndex = 0

        self.mSetUI = [
            self.ui.measureSet1,
            self.ui.measureSet2,
            self.ui.measureSet3,
        ]

        self.plotFunc = {
            'None': None,
            'RA Stability': self.plotRa,
            'DEC Stability': self.plotDec,
            'Temperature': self.plotTemperature,
            'Pressure': self.plotPressure,
            'Humidity': self.plotHumidity,
            'Sky Quality': self.plotSQR,
            'Voltage': self.plotVoltage,
            'Current': self.plotCurrent,
        }

        self.timeScale = {
            '  1s Tick   -  4 min': 1,
            '  2s Ticks  -  8 min': 2,
            '  4s Ticks  - 16 min': 4,
            '  8s Ticks  - 32 min': 8,
            ' 16s Ticks  -  1 hour': 16,
            ' 32s Ticks  -  2 hours': 32,
            ' 64s Ticks  -  4 hours': 64,
            '128s Ticks  -  9 hours': 128,
        }

        # doing the matplotlib embedding
        self.measureMat = self.embedMatplot(self.ui.measure)
        self.measureMat.parentWidget().setStyleSheet(self.BACK_BG)

        self.initConfig()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return: True for test purpose
        """

        if 'measureW' not in self.app.config:
            self.app.config['measureW'] = {}
        config = self.app.config['measureW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 600)
        width = config.get('width', 800)
        self.resize(width, height)
        self.setupButtons()
        self.ui.measureSet1.setCurrentIndex(config.get('measureSet1', 0))
        self.ui.measureSet2.setCurrentIndex(config.get('measureSet2', 0))
        self.ui.measureSet3.setCurrentIndex(config.get('measureSet3', 0))
        self.ui.timeSet.setCurrentIndex(config.get('timeSet', 0))
        self.setCycleRefresh()
        self.showWindow()

        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'measureW' not in self.app.config:
            self.app.config['measureW'] = {}
        config = self.app.config['measureW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()
        config['width'] = self.width()
        config['measureSet1'] = self.ui.measureSet1.currentIndex()
        config['measureSet2'] = self.ui.measureSet2.currentIndex()
        config['measureSet3'] = self.ui.measureSet3.currentIndex()
        config['timeSet'] = self.ui.timeSet.currentIndex()

        return True

    def showWindow(self):
        """

        :return:
        """
        self.show()

        # signals for gui
        self.ui.timeSet.currentIndexChanged.connect(self.setCycleRefresh)
        self.ui.measureSet1.currentIndexChanged.connect(self.setCycleRefresh)
        self.ui.measureSet2.currentIndexChanged.connect(self.setCycleRefresh)
        self.ui.measureSet3.currentIndexChanged.connect(self.setCycleRefresh)
        self.app.update1s.connect(self.cycleRefresh)
        return True

    def closeEvent(self, closeEvent):
        """

        :param closeEvent:
        :return:
        """

        # stop cyclic tasks
        self.app.update1s.disconnect(self.cycleRefresh)

        # save config
        self.storeConfig()

        # signals for gui
        self.ui.timeSet.currentIndexChanged.disconnect(self.setCycleRefresh)
        self.ui.measureSet1.currentIndexChanged.disconnect(
            self.setCycleRefresh)
        self.ui.measureSet2.currentIndexChanged.disconnect(
            self.setCycleRefresh)
        self.ui.measureSet3.currentIndexChanged.disconnect(
            self.setCycleRefresh)

        # remove big object
        plt.close(self.measureMat.figure)

        super().closeEvent(closeEvent)

    def setupButtons(self):
        """
        setupButtons prepares the dynamic content od the buttons in measurement window. it
        write the bottom texts and number as well as the coloring for the actual setting

        :return: success for test purpose
        """

        for mSet in self.mSetUI:
            mSet.clear()
            mSet.setView(PyQt5.QtWidgets.QListView())
            for text in self.plotFunc.keys():
                mSet.addItem(text)

        tSet = self.ui.timeSet
        tSet.clear()
        tSet.setView(PyQt5.QtWidgets.QListView())
        for text in self.timeScale.keys():
            tSet.addItem(text)

        return True

    def setCycleRefresh(self):
        """

        :return: True for test purpose
        """

        self.refreshCounter = self.timeScale[self.ui.timeSet.currentText()]
        self.cycleRefresh()

        return True

    def cycleRefresh(self):
        """

        :return: True for test purpose
        """

        cycle = self.timeScale[self.ui.timeSet.currentText()]
        if not self.refreshCounter % cycle:
            self.drawMeasure(cycle=cycle)
        self.refreshCounter += 1

        return True

    def setupAxes(self, figure=None, numberPlots=3):
        """
        setupAxes cleans up the axes object in figure an setup a new plotting. it draws
        grid, ticks etc.

        :param numberPlots:
        :param figure: axes object of figure
        :return:
        """

        if figure is None:
            return None
        if numberPlots > 3:
            return None
        if numberPlots < 0:
            return None

        for axe in figure.axes:
            axe.cla()
            del axe
            gc.collect()

        figure.clf()
        figure.subplots_adjust(left=0.1, right=0.95, bottom=0.05, top=0.95)
        for i in range(numberPlots):
            self.measureMat.figure.add_subplot(numberPlots,
                                               1,
                                               i + 1,
                                               facecolor=None)

        for axe in figure.axes:
            axe.set_facecolor((0, 0, 0, 0))
            axe.tick_params(colors=self.M_BLUE, labelsize=12)
            axe.spines['bottom'].set_color(self.M_BLUE)
            axe.spines['top'].set_color(self.M_BLUE)
            axe.spines['left'].set_color(self.M_BLUE)
            axe.spines['right'].set_color(self.M_BLUE)

        return figure.axes

    def plotRa(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """
        ylabel = 'delta RA [arcsec]'
        start = -self.NUMBER_POINTS * cycle
        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        axe.plot(
            data['time'][start:-1:cycle],
            data['raJNow'][start:-1:cycle],
            marker=None,
            markersize=3,
            color=self.M_WHITE,
        )
        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.set_ylim(-4, 4)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.1f', ))
        return True

    def plotDec(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """
        ylabel = 'delta DEC [arcsec]'
        start = -self.NUMBER_POINTS * cycle
        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        axe.plot(
            data['time'][start:-1:cycle],
            data['decJNow'][start:-1:cycle],
            marker=None,
            markersize=3,
            color=self.M_WHITE,
        )
        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.set_ylim(-4, 4)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.1f', ))
        return True

    def plotTemperature(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Temperature [deg C]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        if 'sensorWeather' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['sensorWeatherTemp'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            r2, = axe.plot(
                data['time'][start:-1:cycle],
                data['sensorWeatherDew'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            plotList.append(r1)
            plotList.append(r2)
            labelList.append('Sensor Temp')
            labelList.append('Sensor Dew')

        if 'onlineWeather' in self.app.measure.devices:
            r3, = axe.plot(
                data['time'][start:-1:cycle],
                data['onlineWeatherTemp'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_GREEN,
            )
            r4, = axe.plot(
                data['time'][start:-1:cycle],
                data['onlineWeatherDew'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_GREEN,
            )
            plotList.append(r3)
            plotList.append(r4)
            labelList.append('Online Temp')
            labelList.append('Online Dew')

        if 'directWeather' in self.app.measure.devices:
            r5, = axe.plot(
                data['time'][start:-1:cycle],
                data['directWeatherTemp'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_RED,
            )
            r6, = axe.plot(
                data['time'][start:-1:cycle],
                data['directWeatherDew'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_RED,
            )
            plotList.append(r5)
            plotList.append(r6)
            labelList.append('Direct Temp')
            labelList.append('Direct Dew')

        if 'power' in self.app.measure.devices:
            r7, = axe.plot(
                data['time'][start:-1:cycle],
                data['powTemp'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_PINK,
            )
            r8, = axe.plot(
                data['time'][start:-1:cycle],
                data['powDew'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_PINK,
            )
            plotList.append(r7)
            plotList.append(r8)
            labelList.append('Power Temp')
            labelList.append('Power Dew')

        if 'skymeter' in self.app.measure.devices:
            r9, = axe.plot(
                data['time'][start:-1:cycle],
                data['skyTemp'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_YELLOW,
            )
            plotList.append(r9)
            labelList.append('Skymeter Temp')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.margins(y=0.2)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.1f', ))
        return True

    def plotPressure(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Pressure [hPas]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        if 'sensorWeather' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['sensorWeatherPress'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            plotList.append(r1)
            labelList.append('Sensor Temp')

        if 'onlineWeather' in self.app.measure.devices:
            r2, = axe.plot(
                data['time'][start:-1:cycle],
                data['onlineWeatherPress'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_GREEN,
            )
            plotList.append(r2)
            labelList.append('Online Press')

        if 'directWeather' in self.app.measure.devices:
            r3, = axe.plot(
                data['time'][start:-1:cycle],
                data['directWeatherPress'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_RED,
            )
            plotList.append(r3)
            labelList.append('Direct Press')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.margins(y=0.2)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.0f', ))
        return True

    def plotHumidity(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Humidity [%]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        if 'sensorWeather' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['sensorWeatherHum'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            plotList.append(r1)
            labelList.append('Sensor Hum')

        if 'onlineWeather' in self.app.measure.devices:
            r2, = axe.plot(
                data['time'][start:-1:cycle],
                data['onlineWeatherHum'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_GREEN,
            )
            plotList.append(r2)
            labelList.append('Online Hum')

        if 'directWeather' in self.app.measure.devices:
            r3, = axe.plot(
                data['time'][start:-1:cycle],
                data['directWeatherHum'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_RED,
            )
            plotList.append(r3)
            labelList.append('Direct Hum')

        if 'power' in self.app.measure.devices:
            r4, = axe.plot(
                data['time'][start:-1:cycle],
                data['powHum'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_PINK,
            )
            plotList.append(r4)
            labelList.append('Power Hum')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.set_ylim(-0, 100)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.0f', ))
        return True

    def plotSQR(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Sky Quality [mpas]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        if 'skymeter' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['skySQR'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            plotList.append(r1)
            labelList.append('Skymeter SQR')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.margins(y=0.2)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.2f', ))
        return True

    def plotVoltage(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Power Voltage [V]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        if 'power' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['powVolt'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            plotList.append(r1)
            labelList.append('Voltage')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.margins(y=0.2)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.1f', ))
        return True

    def plotCurrent(self, axe=None, title='', data=None, cycle=None):
        """
        drawRaDec show the specific graph for plotting the ra dec deviations. this
        is done with two color and axes to distinguish the values for ra and dec.
        ideally the values are around zero, but the scales of ra and dec axis have to be
        different.

        :param axe: axe for plotting
        :param title: title text
        :param data: data location
        :param cycle: cycle time for measurement
        :return: success
        """

        ylabel = 'Power Current [A]'
        start = -self.NUMBER_POINTS * cycle

        plotList = []
        labelList = []

        axe.set_title(title, color=self.M_BLUE, fontweight='bold', fontsize=16)
        axe.set_ylabel(ylabel,
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        if 'power' in self.app.measure.devices:
            r1, = axe.plot(
                data['time'][start:-1:cycle],
                data['powCurr'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_WHITE,
            )
            r2, = axe.plot(
                data['time'][start:-1:cycle],
                data['powCurr1'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_PINK,
            )
            r3, = axe.plot(
                data['time'][start:-1:cycle],
                data['powCurr2'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_YELLOW,
            )
            r4, = axe.plot(
                data['time'][start:-1:cycle],
                data['powCurr3'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_GREEN,
            )
            r5, = axe.plot(
                data['time'][start:-1:cycle],
                data['powCurr4'][start:-1:cycle],
                marker=None,
                markersize=3,
                color=self.M_RED,
            )
            plotList.append(r1)
            plotList.append(r2)
            plotList.append(r3)
            plotList.append(r4)
            plotList.append(r5)
            labelList.append('Curr Sum')
            labelList.append('Curr 1')
            labelList.append('Curr 2')
            labelList.append('Curr 3')
            labelList.append('Curr 4')

        if not labelList:
            return False

        legend = axe.legend(
            plotList,
            labelList,
            facecolor=self.M_BLACK,
            edgecolor=self.M_BLUE,
        )
        for text in legend.get_texts():
            text.set_color(self.M_BLUE)

        axe.grid(True, color=self.M_GREY, alpha=1)
        axe.margins(y=0.2)
        axe.get_yaxis().set_major_locator(
            ticker.MaxNLocator(
                nbins=8,
                integer=True,
                min_n_ticks=4,
                prune='both',
            ))
        axe.get_yaxis().set_major_formatter(ticker.FormatStrFormatter(
            '%.1f', ))
        return True

    def drawMeasure(self, cycle=1):
        """
        drawMeasure does the basic preparation for making the plot. it checks for borders
        and does finally the content dispatcher. currently there is no chance to implement
        a basic pattern as the graphs differ heavily.

        :param cycle:
        :return: success
        """

        data = self.app.measure.data

        if 'time' not in data:
            return False

        if len(data['time']) < 4:
            return False

        numberPlots = 0
        for mSet in self.mSetUI:
            if mSet.currentText() == 'None':
                continue
            numberPlots += 1

        axes = self.setupAxes(figure=self.measureMat.figure,
                              numberPlots=numberPlots)

        if axes is None:
            return False
        if not numberPlots:
            self.measureMat.figure.canvas.draw()
            return False

        grid = int(self.NUMBER_POINTS / self.NUMBER_XTICKS)
        ratio = cycle * grid
        time_end = data['time'][-1]

        time_ticks = np.arange(-self.NUMBER_XTICKS, 1, 1)
        time_ticks = time_ticks * ratio * 1000000
        time_ticks = time_ticks + time_end
        time_labels = [x.astype(dt).strftime('%H:%M:%S') for x in time_ticks]

        for i, axe in enumerate(axes):
            axe.set_xticks(time_ticks)
            axe.set_xlim(time_ticks[0], time_ticks[-1])
            if i == len(axes) - 1:
                axe.set_xticklabels(time_labels)
            else:
                axe.set_xticklabels([])

        for axe, mSet in zip(axes, self.mSetUI):
            key = mSet.currentText()
            if self.plotFunc[key] is None:
                continue
            self.plotFunc[key](axe=axe,
                               title=mSet.currentText(),
                               data=data,
                               cycle=cycle)
            axe.figure.canvas.draw()

        return True
Exemplo n.º 16
0
class HemisphereWindow(widget.MWidget, HemisphereWindowExt):
    """
    the hemisphere window class handles

    """

    __all__ = [
        'HemisphereWindow',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    RESIZE_FINISHED_TIMEOUT = 0.3

    def __init__(self, app):
        super().__init__()
        self.app = app
        self.ui = hemisphere_ui.Ui_HemisphereDialog()
        self.ui.setupUi(self)
        self.initUI()
        self.mutexDraw = PyQt5.QtCore.QMutex()
        self.operationMode = 'normal'

        self.MODE = dict(normal=dict(horMarker='None',
                                     horColor=self.M_GREEN,
                                     buildPColor=self.M_GREEN_H,
                                     starSize=6,
                                     starColor=self.M_YELLOW_L,
                                     starAnnColor=self.M_WHITE_L),
                         build=dict(horMarker='None',
                                    horColor=self.M_GREEN,
                                    buildPColor=self.M_PINK_H,
                                    starSize=6,
                                    starColor=self.M_YELLOW_L,
                                    starAnnColor=self.M_WHITE_L),
                         horizon=dict(horMarker='o',
                                      horColor=self.M_PINK_H,
                                      buildPColor=self.M_GREEN_L,
                                      starSize=6,
                                      starColor=self.M_YELLOW_L,
                                      starAnnColor=self.M_WHITE_L),
                         star=dict(horMarker='None',
                                   horColor=self.M_GREEN_LL,
                                   buildPColor=self.M_GREEN_L,
                                   starSize=12,
                                   starColor=self.M_YELLOW_H,
                                   starAnnColor=self.M_WHITE_H))

        self.startup = True
        self.resizeTimerValue = -1

        # attributes to be stored in class
        self.pointerAltAz = None
        self.pointerDome = None
        self.pointsBuild = None
        self.pointsBuildAnnotate = list()
        self.starsAlign = None
        self.starsAlignAnnotate = list()
        self.horizonFill = None
        self.horizonMarker = None
        self.meridianSlew = None
        self.meridianTrack = None
        self.horizonLimitHigh = None
        self.horizonLimitLow = None
        self.celestialPath = None

        # doing the matplotlib embedding
        self.hemisphereMat = self.embedMatplot(self.ui.hemisphere)
        self.hemisphereMat.parentWidget().setStyleSheet(self.BACK_BG)
        self.hemisphereBack = None
        self.hemisphereBackStars = None
        self.initConfig()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return: True for test purpose
        """

        if 'hemisphereW' not in self.app.config:
            self.app.config['hemisphereW'] = {}
        config = self.app.config['hemisphereW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 600)
        width = config.get('width', 800)
        self.resize(width, height)
        self.ui.checkShowSlewPath.setChecked(
            config.get('checkShowSlewPath', False))
        self.ui.checkShowMeridian.setChecked(
            config.get('checkShowMeridian', False))
        self.ui.checkShowCelestial.setChecked(
            config.get('checkShowCelestial', False))
        self.ui.checkShowAlignStar.setChecked(
            config.get('checkShowAlignStar', False))
        self.ui.checkUseHorizon.setChecked(config.get('checkUseHorizon',
                                                      False))
        self.ui.showPolar.setChecked(config.get('showPolar', False))
        self.configOperationMode()
        self.showWindow()

        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'hemisphereW' not in self.app.config:
            self.app.config['hemisphereW'] = {}
        config = self.app.config['hemisphereW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()
        config['width'] = self.width()
        config['checkShowSlewPath'] = self.ui.checkShowSlewPath.isChecked()
        config['checkShowMeridian'] = self.ui.checkShowMeridian.isChecked()
        config['checkShowCelestial'] = self.ui.checkShowCelestial.isChecked()
        config['checkShowAlignStar'] = self.ui.checkShowAlignStar.isChecked()
        config['checkUseHorizon'] = self.ui.checkUseHorizon.isChecked()
        config['showPolar'] = self.ui.showPolar.isChecked()

    def closeEvent(self, closeEvent):
        """

        :param closeEvent:
        :return:
        """
        self.app.update10s.disconnect(self.updateAlignStar)
        self.app.update0_1s.disconnect(self.resizeTimer)
        self.storeConfig()

        # signals for gui
        self.ui.checkShowSlewPath.clicked.disconnect(self.drawHemisphere)
        self.ui.checkShowMeridian.clicked.disconnect(self.updateSettings)
        self.ui.checkShowCelestial.clicked.disconnect(self.updateSettings)
        self.ui.checkUseHorizon.clicked.disconnect(self.drawHemisphere)
        self.ui.checkEditNone.clicked.disconnect(self.setOperationMode)
        self.ui.checkEditHorizonMask.clicked.disconnect(self.setOperationMode)
        self.ui.checkEditBuildPoints.clicked.disconnect(self.setOperationMode)
        self.ui.checkPolarAlignment.clicked.disconnect(self.setOperationMode)
        self.ui.checkShowAlignStar.clicked.disconnect(self.drawHemisphere)
        self.ui.checkShowAlignStar.clicked.disconnect(self.configOperationMode)
        self.app.redrawHemisphere.disconnect(self.drawHemisphere)
        self.app.mount.signals.pointDone.disconnect(self.updatePointerAltAz)
        self.app.mount.signals.settingDone.disconnect(self.updateSettings)
        self.app.dome.signals.azimuth.disconnect(self.updateDome)
        self.app.dome.signals.deviceDisconnected.disconnect(self.updateDome)
        self.app.dome.signals.serverDisconnected.disconnect(self.updateDome)

        plt.close(self.hemisphereMat.figure)
        super().closeEvent(closeEvent)

    def resizeEvent(self, event):
        """
        we are using the resize event to reset the timer, which means waiting for
        RESIZE_FINISHED_TIMEOUT in total before redrawing the complete hemisphere.
        as we are using a 0.1s cyclic timer.

        :param event:
        :return:
        """

        super().resizeEvent(event)
        if self.startup:
            self.startup = False
        else:
            self.resizeTimerValue = int(self.RESIZE_FINISHED_TIMEOUT / 0.1)

    def resizeTimer(self):
        """
        the resize timer is a workaround because when resizing the window, the blit
        function needs a new scaled background picture to retrieve. otherwise you will get
        odd picture. unfortunately there is no resize finished event from qt
        framework itself. problem is you never know, when resizing is finished.
        it might be the best way to implement a event filter with mouse and resize
        events to build a resize finished event. so the only quick solution is to wait for
        some time after we got the last resize event and than draw the widgets
        from scratch on.

        :return: True for test purpose
        """

        self.resizeTimerValue -= 1
        if self.resizeTimerValue == 0:
            self.drawHemisphere()

        return True

    def showWindow(self):
        """

        :return:
        """
        # signals for gui
        self.ui.checkShowSlewPath.clicked.connect(self.drawHemisphere)
        self.ui.checkUseHorizon.clicked.connect(self.drawHemisphere)
        self.ui.checkShowAlignStar.clicked.connect(self.drawHemisphere)
        self.app.redrawHemisphere.connect(self.drawHemisphere)
        self.ui.checkShowMeridian.clicked.connect(self.updateSettings)
        self.ui.checkShowCelestial.clicked.connect(self.updateSettings)
        self.app.mount.signals.settingDone.connect(self.updateSettings)
        self.app.mount.signals.pointDone.connect(self.updatePointerAltAz)
        self.app.dome.signals.azimuth.connect(self.updateDome)
        self.app.dome.signals.deviceDisconnected.connect(self.updateDome)
        self.app.dome.signals.serverDisconnected.connect(self.updateDome)
        self.ui.checkEditNone.clicked.connect(self.setOperationMode)
        self.ui.checkEditHorizonMask.clicked.connect(self.setOperationMode)
        self.ui.checkEditBuildPoints.clicked.connect(self.setOperationMode)
        self.ui.checkPolarAlignment.clicked.connect(self.setOperationMode)
        self.ui.checkShowAlignStar.clicked.connect(self.configOperationMode)
        self.app.update10s.connect(self.updateAlignStar)
        self.app.update0_1s.connect(self.resizeTimer)

        # finally setting the mouse handler
        self.hemisphereMat.figure.canvas.mpl_connect('button_press_event',
                                                     self.onMouseDispatcher)
        self.hemisphereMat.figure.canvas.mpl_connect('motion_notify_event',
                                                     self.showMouseCoordinates)
        self.show()
        self.drawHemisphere()
        return True

    def setupAxes(self, widget=None):
        """
        setupAxes cleans up the axes object in figure an setup a new plotting. it draws
        grid, ticks etc.

        :param widget: object of embedded canvas
        :return:
        """

        if widget is None:
            return None

        for axe in widget.figure.axes:
            axe.cla()
            del axe
            gc.collect()

        widget.figure.clf()
        # used constrained_layout = True instead
        # figure.subplots_adjust(left=0.075, right=0.95, bottom=0.1, top=0.975)
        axe = widget.figure.add_subplot(1, 1, 1, facecolor=None)

        axe.set_facecolor((0, 0, 0, 0))
        axe.set_xlim(0, 360)
        axe.set_ylim(0, 90)
        axe.grid(True, color=self.M_GREY)
        axe.tick_params(axis='x',
                        bottom=True,
                        colors=self.M_BLUE,
                        labelsize=12)
        axeTop = axe.twiny()
        axeTop.set_facecolor((0, 0, 0, 0))
        axeTop.set_xlim(0, 360)
        axeTop.tick_params(axis='x',
                           top=True,
                           colors=self.M_BLUE,
                           labelsize=12)
        axeTop.set_xticks(np.arange(0, 361, 45))
        axeTop.grid(axis='both', visible=False)
        axeTop.set_xticklabels(
            ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'])
        axeTop.spines['bottom'].set_color(self.M_BLUE)
        axeTop.spines['top'].set_color(self.M_BLUE)
        axeTop.spines['left'].set_color(self.M_BLUE)
        axeTop.spines['right'].set_color(self.M_BLUE)
        axe.set_xticks(np.arange(0, 361, 45))
        axe.set_xticklabels(
            ['0', '45', '90', '135', '180', '225', '270', '315', '360'])
        axe.tick_params(axis='y',
                        colors=self.M_BLUE,
                        which='both',
                        labelleft=True,
                        labelright=True,
                        labelsize=12)
        axe.set_xlabel('Azimuth in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        axe.set_ylabel('Altitude in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        return axe

    def drawBlit(self):
        """
        There were some optimizations in with regard to drawing speed derived from:
        https://stackoverflow.com/questions/8955869/why-is-plotting-with-matplotlib-so-slow
        so whenever a draw canvas is made, I store the background and painting the
        pointers is done via blit.

        :return: success
        """

        if not self.mutexDraw.tryLock():
            return False

        if self.hemisphereMat.figure.axes and self.hemisphereBackStars:
            axe = self.hemisphereMat.figure.axes[0]
            axe.figure.canvas.restore_region(self.hemisphereBackStars)
            self.pointerAltAz.set_visible(True)
            axe.draw_artist(self.pointerAltAz)
            axe.draw_artist(self.pointerDome)
            axe.figure.canvas.blit(axe.bbox)

        self.mutexDraw.unlock()
        return True

    def drawBlitStars(self):
        """
        The alignment stars were the second layer to be draw.

        There were some optimizations in with regard to drawing speed derived from:
        https://stackoverflow.com/questions/8955869/why-is-plotting-with-matplotlib-so-slow
        so whenever a draw canvas is made, I store the background and painting the
        pointers is done via blit.

        :return: success
        """

        if not self.mutexDraw.tryLock():
            return False

        if self.hemisphereMat.figure.axes and self.hemisphereBack:
            axe = self.hemisphereMat.figure.axes[0]
            axe.figure.canvas.restore_region(self.hemisphereBack)
            axe.draw_artist(self.starsAlign)
            for annotation in self.starsAlignAnnotate:
                axe.draw_artist(annotation)
            axe.figure.canvas.blit(axe.bbox)
            self.hemisphereBackStars = axe.figure.canvas.copy_from_bbox(
                axe.bbox)

        self.mutexDraw.unlock()
        return True

    def updateCelestialPath(self):
        """
        updateCelestialPath is called whenever an update of settings from mount are given.
        it takes the actual values and corrects the point in window if window is in
        show status.
        If the object is not created, the routing returns false.

        :return: needs drawing
        """

        if self.celestialPath is None:
            return False

        isVisible = self.celestialPath.get_visible()
        newVisible = self.ui.checkShowCelestial.isChecked()
        needDraw = isVisible != newVisible
        self.celestialPath.set_visible(newVisible)

        return needDraw

    def updateMeridian(self, sett):
        """
        updateMeridian is called whenever an update of settings from mount are given. it
        takes the actual values and corrects the point in window if window is in
        show status.
        If the object is not created, the routing returns false.

        :param sett: settings reference from mount
        :return: needs drawing
        """

        slew = sett.meridianLimitSlew
        track = sett.meridianLimitTrack
        if slew is None or track is None:
            return False
        if self.meridianTrack is None:
            return False
        if self.meridianSlew is None:
            return False

        isVisible = self.meridianTrack.get_visible()
        newVisible = self.ui.checkShowMeridian.isChecked()
        needDraw = isVisible != newVisible

        self.meridianTrack.set_visible(newVisible)
        self.meridianSlew.set_visible(newVisible)

        self.meridianTrack.set_xy((180 - track, 0))
        self.meridianSlew.set_xy((180 - slew, 0))

        aktTrack = self.meridianTrack.get_width() / 2
        aktSlew = self.meridianSlew.get_width() / 2

        needDraw = needDraw or (track != aktTrack) or (slew != aktSlew)

        self.meridianTrack.set_width(2 * track)
        self.meridianSlew.set_width(2 * slew)

        return needDraw

    def updateHorizonLimits(self, sett):
        """
        updateHorizonLimits is called whenever an update of settings from mount are given. it
        takes updateHorizonLimits actual values and corrects the point in window if window
        is in show status.
        If the object is not created, the routing returns false.

        :param sett: settings reference from mount
        :return: success
        """

        high = sett.horizonLimitHigh
        low = sett.horizonLimitLow
        if high is None or low is None:
            return False
        if self.horizonLimitLow is None:
            return False
        if self.horizonLimitHigh is None:
            return False

        aktHigh = self.horizonLimitHigh.get_xy()[1]
        aktLow = self.horizonLimitLow.get_height()

        needDraw = aktHigh != high or aktLow != low

        self.horizonLimitHigh.set_xy((0, high))
        self.horizonLimitHigh.set_height(90 - high)
        self.horizonLimitLow.set_xy((0, 0))
        self.horizonLimitLow.set_height(low)

        return needDraw

    def updateSettings(self):
        """
        updateSettings renders all static settings upon signals received and aggregates the
        need of a drawing. the called methods have to detect if something changed to
        determine if a redraw has to be done. this is done to reduce runtime and preserve
        low CPU processing.

        :return: needs draw
        """

        sett = self.app.mount.setting

        suc = self.updateCelestialPath()
        suc = self.updateHorizonLimits(sett) or suc
        suc = self.updateMeridian(sett) or suc

        if suc:
            self.drawHemisphere()
        return suc

    def updatePointerAltAz(self):
        """
        updatePointerAltAz is called whenever an update of coordinates from mount are
        given. it takes the actual values and corrects the point in window if window is in
        show status.
        If the object is not created, the routing returns false.

        There were some optimizations in with regard to drawing speed derived from:
        https://stackoverflow.com/questions/8955869/why-is-plotting-with-matplotlib-so-slow
        so whenever a draw canvas is made, I store the background and painting the
        pointers is done via blit.

        :return: success
        """

        obsSite = self.app.mount.obsSite
        if obsSite.Alt is None:
            return False
        if obsSite.Az is None:
            return False
        if self.pointerAltAz is None:
            return False
        alt = obsSite.Alt.degrees
        az = obsSite.Az.degrees
        self.pointerAltAz.set_data((az, alt))

        self.drawBlit()

        return True

    def updateDome(self, azimuth):
        """
        updateDome is called whenever an update of coordinates from dome are given.
        it takes the actual values and corrects the point in window if window is in
        show status.
        If the object is not created, the routing returns false.

        :param azimuth:
        :return: success
        """

        if self.pointerDome is None:
            return False
        if not isinstance(azimuth, (int, float)):
            self.pointerDome.set_visible(False)
            return False

        visible = self.app.mainW.deviceStat['dome']

        self.pointerDome.set_xy((azimuth - 15, 1))
        self.pointerDome.set_visible(visible)

        self.drawBlit()

        return True

    def updateAlignStar(self):
        """
        updateAlignStar is called whenever an update of coordinates from mount are
        given. it takes the actual values and corrects the point in window if window is in
        show status.
        If the object is not created, the routing returns false.

        :return: success
        """

        if not self.ui.checkShowAlignStar.isChecked():
            return False
        if self.starsAlign is None:
            return False
        if self.starsAlignAnnotate is None:
            return False

        axes = self.hemisphereMat.figure.axes[0]
        hip = self.app.hipparcos
        hip.calculateAlignStarPositionsAltAz()
        self.starsAlign.set_data(hip.az, hip.alt)
        for i, starAnnotation in enumerate(self.starsAlignAnnotate):
            starAnnotation.remove()
        self.starsAlignAnnotate = list()

        # due to the fact that all annotation are only shown if in axes when coordinate
        # are in data, after some time, no annotation will be shown, because just moved.
        # therefore we add each time the annotation again.

        visible = self.ui.checkShowAlignStar.isChecked()
        for alt, az, name in zip(hip.alt, hip.az, hip.name):
            annotation = axes.annotate(
                name,
                xy=(az, alt),
                xytext=(2, 2),
                textcoords='offset points',
                xycoords='data',
                color='#808080',
                fontsize=12,
                clip_on=True,
                visible=visible,
            )
            self.starsAlignAnnotate.append(annotation)
        self.drawBlitStars()
        return True

    def clearHemisphere(self):
        """
        clearHemisphere is called when after startup the location of the mount is changed
        to reconstruct correctly the hemisphere window

        :return:
        """

        self.pointsBuild = None
        self.app.data.clearBuildP()
        self.drawHemisphere()

    def staticHorizon(self, axes=None):
        """
        staticHorizon draw the horizon line. in case of a polar plot it will be reversed,
        which mean the background will be green and to horizon polygon will be drawn in
        background color

        :param axes: matplotlib axes object
        :return:
        """

        showHorizon = self.ui.checkUseHorizon.isChecked()

        if not (self.app.data.horizonP and showHorizon):
            return False

        alt, az = zip(*self.app.data.horizonP)
        alt = np.array(alt)
        az = np.array(az)

        self.horizonFill, = axes.fill(az, alt, color='#002000', zorder=-20)
        self.horizonMarker, = axes.plot(
            az,
            alt,
            color=self.MODE[self.operationMode]['horColor'],
            marker=self.MODE[self.operationMode]['horMarker'],
            zorder=-20,
            lw=3)

        return True

    def staticModelData(self, axes=None):
        """
        staticModelData draw in the chart the build points and their index as annotations

        :param axes: matplotlib axes object
        :return: success
        """

        if not self.app.data.buildP:
            return False

        alt, az = zip(*self.app.data.buildP)
        alt = np.array(alt)
        az = np.array(az)

        # show line path pf slewing
        if self.ui.checkShowSlewPath.isChecked():
            ls = ':'
            lw = 1
        else:
            ls = ''
            lw = 0

        self.pointsBuild, = axes.plot(
            az,
            alt,
            marker=self.markerPoint(),
            markersize=9,
            linestyle=ls,
            lw=lw,
            fillstyle='none',
            color=self.MODE[self.operationMode]['buildPColor'],
            zorder=20,
        )
        self.pointsBuildAnnotate = list()
        for i, AltAz in enumerate(zip(az, alt)):
            annotation = axes.annotate(
                '{0:2d}'.format(i),
                xy=AltAz,
                xytext=(2, -10),
                textcoords='offset points',
                color='#E0E0E0',
                zorder=10,
            )
            self.pointsBuildAnnotate.append(annotation)
        return True

    def staticCelestialEquator(self, axes=None):
        """
        staticCelestialEquator draw ra / dec lines on the chart

        :param axes: matplotlib axes object
        :return: success
        """

        # draw celestial equator
        visible = self.ui.checkShowCelestial.isChecked()
        celestial = self.app.data.generateCelestialEquator()
        alt, az = zip(*celestial)

        self.celestialPath, = axes.plot(az,
                                        alt,
                                        '.',
                                        markersize=1,
                                        fillstyle='none',
                                        color='#808080',
                                        visible=visible)
        return True

    def staticMeridianLimits(self, axes=None):
        """

        :param axes: matplotlib axes object
        :return: success
        """
        # draw meridian limits
        if self.app.mount.setting.meridianLimitSlew is not None:
            slew = self.app.mount.setting.meridianLimitSlew
        else:
            slew = 0
        visible = self.ui.checkShowMeridian.isChecked()
        self.meridianSlew = mpatches.Rectangle((180 - slew, 0),
                                               2 * slew,
                                               90,
                                               zorder=-5,
                                               color='#00008080',
                                               visible=visible)
        axes.add_patch(self.meridianSlew)
        if self.app.mount.setting.meridianLimitTrack is not None:
            track = self.app.mount.setting.meridianLimitTrack
        else:
            track = 0
        self.meridianTrack = mpatches.Rectangle((180 - track, 0),
                                                2 * track,
                                                90,
                                                zorder=-10,
                                                color='#80800080',
                                                visible=visible)
        axes.add_patch(self.meridianTrack)
        return True

    def staticHorizonLimits(self, axes=None):
        """

        :param axes: matplotlib axes object
        :return: success
        """

        if self.app.mount.setting.horizonLimitHigh is not None:
            high = self.app.mount.setting.horizonLimitHigh
        else:
            high = 90
        if self.app.mount.setting.horizonLimitLow is not None:
            low = self.app.mount.setting.horizonLimitLow
        else:
            low = 0
        self.horizonLimitHigh = mpatches.Rectangle((0, high),
                                                   360,
                                                   90 - high,
                                                   zorder=-30,
                                                   color='#60383880',
                                                   visible=True)
        self.horizonLimitLow = mpatches.Rectangle((0, 0),
                                                  360,
                                                  low,
                                                  zorder=-30,
                                                  color='#60383880',
                                                  visible=True)
        axes.add_patch(self.horizonLimitHigh)
        axes.add_patch(self.horizonLimitLow)
        return True

    def drawHemisphereStatic(self, axes=None):
        """
         drawHemisphereStatic renders the static part of the hemisphere window and puts
         all drawing on the static plane. the content consist of:
            - modeldata points
            - horizon mask
            - celestial paths
            - meridian limits
        with all their styles an coloring

        :param axes: matplotlib axes object
        :return: success
        """

        if not self.mutexDraw.tryLock():
            return False

        self.staticHorizon(axes=axes)
        self.staticCelestialEquator(axes=axes)
        self.staticMeridianLimits(axes=axes)
        self.staticHorizonLimits(axes=axes)
        self.staticModelData(axes=axes)

        axes.figure.canvas.draw()
        axes.figure.canvas.flush_events()
        self.hemisphereBack = axes.figure.canvas.copy_from_bbox(axes.bbox)

        self.mutexDraw.unlock()

        return True

    def drawHemisphereMoving(self, axes=None):
        """
        drawHemisphereMoving is rendering the moving part which consists of:
            - pointer: where the mount points to
            - dome widget: which shows the position of the dome opening
        the dynamic ones are located on a separate plane to improve rendering speed,
        because we update this part very often.

        :param axes: matplotlib axes object
        :return:
        """

        self.pointerAltAz, = axes.plot(
            180,
            45,
            zorder=10,
            color='#FF00FF',
            marker=self.markerAltAz(),
            markersize=25,
            linestyle='none',
            fillstyle='none',
            clip_on=True,
            visible=False,
        )

        self.pointerDome = mpatches.Rectangle((165, 1),
                                              30,
                                              88,
                                              zorder=-30,
                                              color='#40404080',
                                              lw=3,
                                              clip_on=True,
                                              fill=True,
                                              visible=False)
        axes.add_patch(self.pointerDome)
        return True

    def drawAlignmentStars(self, axes=None):
        """
        drawAlignmentStars is rendering the alignment star map. this moves over time with
        the speed of earth turning. so we have to update the rendering, but on low speed
        without having any user interaction.

        :param axes: matplotlib axes object
        :return: true for test purpose
        """
        visible = self.ui.checkShowAlignStar.isChecked()
        self.starsAlignAnnotate = list()
        hip = self.app.hipparcos
        hip.calculateAlignStarPositionsAltAz()
        self.starsAlign, = axes.plot(
            hip.az,
            hip.alt,
            marker=self.markerStar(),
            markersize=7,
            linestyle='',
            color=self.MODE[self.operationMode]['starColor'],
            zorder=-20,
            visible=visible,
        )
        for alt, az, name in zip(hip.alt, hip.az, hip.name):
            annotation = axes.annotate(
                name,
                xy=(az, alt),
                xytext=(2, 2),
                textcoords='offset points',
                xycoords='data',
                color='#808080',
                fontsize=12,
                clip_on=True,
                visible=visible,
            )
            self.starsAlignAnnotate.append(annotation)
        self.drawBlitStars()
        return True

    def drawHemisphere(self):
        """
        drawHemisphere is the basic renderer for all items and widgets in the hemisphere
        window. it takes care of drawing the grid, enables three layers of transparent
        widgets for static content, moving content and star maps. this is mainly done to
        get a reasonable performance when redrawing the canvas. in addition it initializes
        the objects for points markers, patches, lines etc. for making the window nice
        and user friendly.
        the user interaction on the hemisphere windows is done by the event handler of
        matplotlib itself implementing an on Mouse handler, which takes care of functions.

        :return: nothing
        """

        # clearing axes before drawing, only static visible, dynamic only when content
        # is available. visibility is handled with their update method
        self.hemisphereMat.figure.canvas.draw()
        axes = self.setupAxes(widget=self.hemisphereMat)
        # calling renderer
        self.drawHemisphereStatic(axes=axes)
        self.drawHemisphereMoving(axes=axes)
        self.drawAlignmentStars(axes=axes)
Exemplo n.º 17
0
class KeypadWindow(widget.MWidget):
    """
    the KeypadWindow window class handles

    """

    __all__ = ['KeypadWindow',
               ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):
        super().__init__()
        self.app = app

        self.ui = keypad_ui.Ui_KeypadDialog()
        self.ui.setupUi(self)
        self.initUI()

        # getting a new browser object
        self.browser = PyQt5.QtWebEngineWidgets.QWebEngineView()

        # adding it to window widget
        self.ui.keypad.addWidget(self.browser)

        # avoid flickering in white
        self.browser.setVisible(False)
        self.browser.page().setBackgroundColor(PyQt5.QtCore.Qt.transparent)

        self.initConfig()
        self.showWindow()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return: True for test purpose
        """

        if 'keypadW' not in self.app.config:
            self.app.config['keypadW'] = {}

        config = self.app.config['keypadW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 500)
        width = config.get('width', 260)
        self.resize(width, height)
        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'keypadW' not in self.app.config:
            self.app.config['keypadW'] = {}
        config = self.app.config['keypadW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()
        config['width'] = self.width()

        return True

    def closeEvent(self, closeEvent):
        """

        :param closeEvent:
        :return:
        """

        # save config
        self.storeConfig()

        # gui signals
        self.browser.loadFinished.disconnect(self.loadFinished)

        # remove big object
        self.browser = None

        super().closeEvent(closeEvent)

    def showWindow(self):
        """

        :return:
        """

        self.browser.loadFinished.connect(self.loadFinished)
        self.showUrl()
        self.show()

    def loadFinished(self):
        """

        :return:
        """

        self.browser.setVisible(True)

    def showUrl(self):
        """

        :return: success
        """

        host = self.app.mainW.ui.mountHost.text()

        if not host:
            return False

        file = f'qrc:/webif/virtkeypad.html?host={host}'
        self.browser.load(PyQt5.QtCore.QUrl(file))

        return True
Exemplo n.º 18
0
class FlipFlatIndi(IndiClass):
    """
    the class FlipFlatIndi inherits all information and handling of the FlipFlat device

        >>> f = FlipFlatIndi(app=None)
    """

    __all__ = [
        'FlipFlatIndi',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1 seconds for setting indi server
    UPDATE_RATE = 1

    def __init__(self, app=None, signals=None, data=None):
        super().__init__(app=app)

        self.signals = signals
        self.data = data

    def setUpdateConfig(self, deviceName):
        """
        _setUpdateRate corrects the update rate of weather devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """

        if deviceName != self.name:
            return False

        if self.device is None:
            return False

        update = self.device.getNumber('PERIOD_MS')

        if 'PERIOD' not in update:
            return False

        if update.get('PERIOD', 0) == self.UPDATE_RATE:
            return True

        update['PERIOD'] = self.UPDATE_RATE
        suc = self.client.sendNewNumber(deviceName=deviceName,
                                        propertyName='PERIOD_MS',
                                        elements=update)
        return suc

    def sendCoverPark(self, park=True):
        """

        :param park:
        :return: success
        """

        if self.device is None:
            return False

        cover = self.device.getSwitch('CAP_PARK')

        cover['UNPARK'] = not park
        cover['PARK'] = park
        suc = self.client.sendNewSwitch(
            deviceName=self.name,
            propertyName='CAP_PARK',
            elements=cover,
        )
        return suc
Exemplo n.º 19
0
class FilterIndi(IndiClass):
    """
    the class FilterIndi inherits all information and handling of the FilterWheel device

        >>> f = FilterIndi(app=None)
    """

    __all__ = [
        'FilterIndi',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1 seconds for setting indi server
    UPDATE_RATE = 1

    def __init__(self, app=None, signals=None, data=None):
        super().__init__(app=app, data=data)

        self.signals = signals
        self.data = data

    def setUpdateConfig(self, deviceName):
        """
        _setUpdateRate corrects the update rate of weather devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """

        if deviceName != self.name:
            return False

        if self.device is None:
            return False

        update = self.device.getNumber('PERIOD_MS')

        if 'PERIOD' not in update:
            return False

        if update.get('PERIOD', 0) == self.UPDATE_RATE:
            return True

        update['PERIOD'] = self.UPDATE_RATE
        suc = self.client.sendNewNumber(deviceName=deviceName,
                                        propertyName='PERIOD_MS',
                                        elements=update)
        return suc

    def sendFilterNumber(self, filterNumber=1):
        """
        sendFilterNumber send the desired filter number

        :param filterNumber:
        :return: success
        """

        # setting fast mode:
        filterNo = self.device.getNumber('FILTER_SLOT')
        filterNo['FILTER_SLOT_VALUE'] = filterNumber
        suc = self.client.sendNewNumber(
            deviceName=self.name,
            propertyName='FILTER_SLOT',
            elements=filterNo,
        )

        return suc
Exemplo n.º 20
0
class DomeIndi(IndiClass):
    """
    the class Dome inherits all information and handling of the Dome device. there will be
    some parameters who will define the slewing position of the dome relating to the mount.

        >>> dome = DomeIndi(app=None)
    """

    __all__ = [
        'DomeIndi',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1000 milli seconds for setting indi server
    UPDATE_RATE = 1000

    def __init__(self, app=None, signals=None, data=None):
        super().__init__(app=app, data=data)

        self.signals = signals
        self.data = data
        self.settlingTime = 0
        self.azimuth = -1
        self.slewing = False

        self.app.update1s.connect(self.updateStatus)

        self.settlingWait = PyQt5.QtCore.QTimer()
        self.settlingWait.setSingleShot(True)
        self.settlingWait.timeout.connect(self.waitSettlingAndEmit)

    @property
    def settlingTime(self):
        return self._settlingTime * 1000

    @settlingTime.setter
    def settlingTime(self, value):
        self._settlingTime = value

    def setUpdateConfig(self, deviceName):
        """
        _setUpdateRate corrects the update rate of dome devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """

        if deviceName != self.name:
            return False

        if self.device is None:
            return False

        # setting polling updates in driver
        update = self.device.getNumber('POLLING_PERIOD')

        if 'PERIOD_MS' not in update:
            return False

        if update.get('PERIOD_MS', 0) == self.UPDATE_RATE:
            return True

        update['PERIOD_MS'] = self.UPDATE_RATE
        suc = self.client.sendNewNumber(
            deviceName=deviceName,
            propertyName='POLLING_PERIOD',
            elements=update,
        )

        return suc

    def updateStatus(self):
        """
        updateStatus emits the actual azimuth status every 3 second in case of opening a
        window and get the signals late connected as INDI does nt repeat any signal of it's
        own

        :return: true for test purpose
        """

        if not self.client.connected:
            return False

        self.signals.azimuth.emit(self.azimuth)

        return True

    def waitSettlingAndEmit(self):
        """
        waitSettlingAndEmit emit the signal for slew finished

        :return: true for test purpose
        """
        self.signals.slewFinished.emit()
        return True

    def updateNumber(self, deviceName, propertyName):
        """
        updateNumber is called whenever a new number is received in client. it runs
        through the device list and writes the number data to the according locations.

        :param deviceName:
        :param propertyName:
        :return:
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getNumber(propertyName).items():
            key = propertyName + '.' + element
            self.data[key] = value
            # print(propertyName, element, value)

            if element != 'DOME_ABSOLUTE_POSITION':
                continue

            # starting condition: don't do anything
            if self.azimuth == -1:
                self.azimuth = value
                continue

            # send trigger for new data
            self.signals.azimuth.emit(self.azimuth)

            # calculate the stop slewing condition
            isSlewing = (self.device.ABS_DOME_POSITION['state'] == 'Busy')
            if isSlewing:
                self.signals.message.emit('slewing')
            else:
                self.signals.message.emit('')

            if self.slewing and not isSlewing:
                # start timer for settling time and emit signal afterwards
                self.settlingWait.start(self.settlingTime)

            # store for the next cycle
            self.azimuth = value
            self.slewing = isSlewing

        return True

    def slewToAltAz(self, altitude=0, azimuth=0):
        """
        slewToAltAz sends a command to the dome to move to azimuth / altitude. if a dome
        does support this

        :param altitude:
        :param azimuth:
        :return: success
        """

        if self.device is None:
            return False

        if self.name is None or not self.name:
            return False

        position = self.device.getNumber('ABS_DOME_POSITION')

        if 'DOME_ABSOLUTE_POSITION' not in position:
            return False

        position['DOME_ABSOLUTE_POSITION'] = azimuth

        suc = self.client.sendNewNumber(
            deviceName=self.name,
            propertyName='ABS_DOME_POSITION',
            elements=position,
        )

        if suc:
            self.slewing = True

        return suc
Exemplo n.º 21
0
class Focuser:

    __all__ = [
        'Focuser',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):

        self.app = app
        self.threadPool = app.threadPool
        self.signals = FocuserSignals()

        self.data = {}
        self.framework = None
        self.run = {
            'indi': FocuserIndi(self.app, self.signals, self.data),
        }
        self.name = ''

        self.host = ('localhost', 7624)
        self.isGeometry = False

        # signalling from subclasses to main
        self.run['indi'].client.signals.serverConnected.connect(
            self.signals.serverConnected)
        self.run['indi'].client.signals.serverDisconnected.connect(
            self.signals.serverDisconnected)
        self.run['indi'].client.signals.deviceConnected.connect(
            self.signals.deviceConnected)
        self.run['indi'].client.signals.deviceDisconnected.connect(
            self.signals.deviceDisconnected)

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, value):
        self._host = value
        if self.framework in self.run.keys():
            self.run[self.framework].host = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        if self.framework in self.run.keys():
            self.run[self.framework].name = value

    def startCommunication(self):
        """

        """

        if self.framework not in self.run.keys():
            return False

        suc = self.run[self.framework].startCommunication()
        return suc

    def stopCommunication(self):
        """

        """

        if self.framework not in self.run.keys():
            return False

        suc = self.run[self.framework].stopCommunication()
        return suc
Exemplo n.º 22
0
class ImageWindow(widget.MWidget):
    """
    the image window class handles fits image loading, stretching, zooming and handles
    the gui interface for display. both wcs and pixel coordinates will be used.

    """

    __all__ = [
        'ImageWindow',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):
        super().__init__()
        self.app = app
        self.ui = image_ui.Ui_ImageDialog()
        self.ui.setupUi(self)
        self.initUI()
        self.signals = ImageWindowSignals()

        self.imageFileName = ''
        self.imageFileNameOld = ''
        self.imageStack = None
        self.raStack = 0
        self.decStack = 0
        self.angleStack = 0
        self.scaleStack = 0
        self.numberStack = 0
        self.folder = ''

        self.deviceStat = {
            'expose': False,
            'exposeN': False,
            'solve': False,
        }
        self.colorMaps = {
            'Grey': 'gray',
            'Cool': 'plasma',
            'Rainbow': 'rainbow',
            'Spectral': 'nipy_spectral',
        }

        self.stretchValues = {
            'Low X': (98, 99.999),
            'Low': (90, 99.995),
            'Mid': (50, 99.99),
            'High': (20, 99.98),
            'Super': (10, 99.9),
            'Super X': (1, 99.8),
        }

        self.zoomLevel = {
            ' 1x Zoom': 1,
            ' 2x Zoom': 2,
            ' 4x Zoom': 4,
            ' 8x Zoom': 8,
            '16x Zoom': 16,
        }

        self.imageMat = self.embedMatplot(self.ui.image)
        self.imageMat.parentWidget().setStyleSheet(self.BACK_BG)

        # cyclic updates
        self.app.update1s.connect(self.updateWindowsStats)

        self.initConfig()
        self.showWindow()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method. if not entry is already in the
        config dict, it will be created first.
        default values will be set in case of missing parameters.
        screen size will be set as well as the window position. if the window position is
        out of the current screen size (because of copy configs or just because the screen
        resolution was changed) the window will be repositioned so that it will be visible.

        :return: True for test purpose
        """

        if 'imageW' not in self.app.config:
            self.app.config['imageW'] = {}
        config = self.app.config['imageW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 600)
        width = config.get('width', 800)
        self.resize(width, height)

        self.setupDropDownGui()
        self.ui.color.setCurrentIndex(config.get('color', 0))
        self.ui.zoom.setCurrentIndex(config.get('zoom', 0))
        self.ui.stretch.setCurrentIndex(config.get('stretch', 0))
        self.imageFileName = config.get('imageFileName', '')
        self.folder = self.app.mwGlob.get('imageDir', '')
        self.ui.checkUseWCS.setChecked(config.get('checkUseWCS', False))
        self.ui.checkStackImages.setChecked(
            config.get('checkStackImages', False))
        self.ui.checkShowCrosshair.setChecked(
            config.get('checkShowCrosshair', False))
        self.ui.checkShowGrid.setChecked(config.get('checkShowGrid', True))
        self.ui.checkAutoSolve.setChecked(config.get('checkAutoSolve', False))
        self.ui.checkEmbedData.setChecked(config.get('checkEmbedData', False))

        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'imageW' not in self.app.config:
            self.app.config['imageW'] = {}
        config = self.app.config['imageW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()
        config['width'] = self.width()
        config['color'] = self.ui.color.currentIndex()
        config['zoom'] = self.ui.zoom.currentIndex()
        config['stretch'] = self.ui.stretch.currentIndex()
        config['imageFileName'] = self.imageFileName
        config['checkUseWCS'] = self.ui.checkUseWCS.isChecked()
        config['checkStackImages'] = self.ui.checkStackImages.isChecked()
        config['checkShowCrosshair'] = self.ui.checkShowCrosshair.isChecked()
        config['checkShowGrid'] = self.ui.checkShowGrid.isChecked()
        config['checkAutoSolve'] = self.ui.checkAutoSolve.isChecked()
        config['checkEmbedData'] = self.ui.checkEmbedData.isChecked()

        return True

    def showWindow(self):
        """
        showWindow prepares all data for showing the window, plots the image and show it.
        afterwards all necessary signal / slot connections will be established.

        :return: true for test purpose
        """

        self.showCurrent()
        self.show()

        # gui signals
        self.ui.load.clicked.connect(self.selectImage)
        self.ui.color.currentIndexChanged.connect(self.showCurrent)
        self.ui.stretch.currentIndexChanged.connect(self.showCurrent)
        self.ui.zoom.currentIndexChanged.connect(self.showCurrent)
        self.ui.checkUseWCS.clicked.connect(self.showCurrent)
        self.ui.checkShowGrid.clicked.connect(self.showCurrent)
        self.ui.checkShowCrosshair.clicked.connect(self.showCurrent)
        self.ui.solve.clicked.connect(self.solveCurrent)
        self.ui.expose.clicked.connect(self.exposeImage)
        self.ui.exposeN.clicked.connect(self.exposeImageN)
        self.ui.abortImage.clicked.connect(self.abortImage)
        self.ui.abortSolve.clicked.connect(self.abortSolve)
        self.signals.showCurrent.connect(self.showCurrent)
        self.signals.showImage.connect(self.showImage)
        self.signals.solveImage.connect(self.solveImage)

        return True

    def closeEvent(self, closeEvent):
        """
        closeEvent overlays the window close event of qt. first it stores all persistent
        data for the windows and its functions, than removes al signal / slot connections
        removes the matplotlib embedding and finally calls the parent calls for handling
        the framework close event.

        :param closeEvent:
        :return: True for test purpose
        """

        self.storeConfig()

        # gui signals
        self.ui.load.clicked.disconnect(self.selectImage)
        self.ui.color.currentIndexChanged.disconnect(self.showCurrent)
        self.ui.stretch.currentIndexChanged.disconnect(self.showCurrent)
        self.ui.zoom.currentIndexChanged.disconnect(self.showCurrent)
        self.ui.checkUseWCS.clicked.disconnect(self.showCurrent)
        self.ui.checkShowGrid.clicked.disconnect(self.showCurrent)
        self.ui.checkShowCrosshair.clicked.disconnect(self.showCurrent)
        self.ui.solve.clicked.disconnect(self.solveCurrent)
        self.ui.expose.clicked.disconnect(self.exposeImage)
        self.ui.exposeN.clicked.disconnect(self.exposeImageN)
        self.ui.abortImage.clicked.disconnect(self.abortImage)
        self.ui.abortSolve.clicked.disconnect(self.abortSolve)
        self.signals.showCurrent.disconnect(self.showCurrent)
        self.signals.showImage.disconnect(self.showImage)
        self.signals.solveImage.disconnect(self.solveImage)

        plt.close(self.imageMat.figure)
        super().closeEvent(closeEvent)

    def setupDropDownGui(self):
        """
        setupDropDownGui handles the population of list for image handling.

        :return: success for test
        """

        self.ui.color.clear()
        self.ui.color.setView(PyQt5.QtWidgets.QListView())
        for text in self.colorMaps:
            self.ui.color.addItem(text)

        self.ui.zoom.clear()
        self.ui.zoom.setView(PyQt5.QtWidgets.QListView())
        for text in self.zoomLevel:
            self.ui.zoom.addItem(text)

        self.ui.stretch.clear()
        self.ui.stretch.setView(PyQt5.QtWidgets.QListView())
        for text in self.stretchValues:
            self.ui.stretch.addItem(text)

        return True

    def updateWindowsStats(self):
        """
        updateWindowsStats changes dynamically the enable and disable of user gui elements
        depending of the actual state of processing

        :return: true for test purpose

        """

        if self.deviceStat['expose']:
            self.ui.exposeN.setEnabled(False)
            self.ui.load.setEnabled(False)
            self.ui.abortImage.setEnabled(True)

        elif self.deviceStat['exposeN']:
            self.ui.expose.setEnabled(False)
            self.ui.load.setEnabled(False)
            self.ui.abortImage.setEnabled(True)
        else:
            self.ui.solve.setEnabled(True)
            self.ui.expose.setEnabled(True)
            self.ui.exposeN.setEnabled(True)
            self.ui.load.setEnabled(True)
            self.ui.abortImage.setEnabled(False)

        if self.deviceStat['solve']:
            self.ui.abortSolve.setEnabled(True)
        else:
            self.ui.abortSolve.setEnabled(False)

        if not self.app.mainW.deviceStat['camera']:
            self.ui.expose.setEnabled(False)
            self.ui.exposeN.setEnabled(False)

        if not self.app.mainW.deviceStat['astrometry']:
            self.ui.solve.setEnabled(False)

        if self.deviceStat['expose']:
            self.changeStyleDynamic(self.ui.expose, 'running', 'true')

        elif self.deviceStat['exposeN']:
            self.changeStyleDynamic(self.ui.exposeN, 'running', 'true')

        else:
            self.changeStyleDynamic(self.ui.expose, 'running', 'false')
            self.changeStyleDynamic(self.ui.exposeN, 'running', 'false')

        if self.deviceStat['solve']:
            self.changeStyleDynamic(self.ui.solve, 'running', 'true')
        else:
            self.changeStyleDynamic(self.ui.solve, 'running', 'false')

        return True

    def selectImage(self):
        """
        selectImage does a dialog to choose a FITS file for viewing. The file will not
        be loaded, just the full filepath will be stored. if succeeding, the signal for
        displaying the image will be emitted.

        :return: success
        """

        loadFilePath, name, ext = self.openFile(
            self,
            'Select image file',
            self.folder,
            'FITS files (*.fit*)',
            enableDir=True,
        )
        if not name:
            return False

        self.ui.imageFileName.setText(name)
        self.imageFileName = loadFilePath
        self.app.message.emit(f'Image [{name}] selected', 0)
        self.ui.checkUseWCS.setChecked(False)
        self.folder = os.path.dirname(loadFilePath)
        self.signals.showImage.emit(self.imageFileName)

        return True

    def writeHeaderToGUI(self, header=None):
        """
        writeHeaderToGUI tries to read relevant values from FITS header and possible
        replace values and write them to the imageW gui

        :param header: header of fits file
        :return: hasCelestial, hasDistortion
        """

        name = header.get('OBJECT', '').upper()
        self.ui.object.setText(f'{name}')

        ra = Angle(degrees=header.get('RA', 0))
        dec = Angle(degrees=header.get('DEC', 0))

        # ra will be in hours
        self.ui.ra.setText(f'{ra.hstr(warn=False)}')
        self.ui.dec.setText(f'{dec.dstr()}')

        scale = header.get('SCALE', 0)
        rotation = header.get('ANGLE', 0)
        self.ui.scale.setText(f'{scale:5.3f}')
        self.ui.rotation.setText(f'{rotation:6.3f}')

        ccdTemp = header.get('CCD-TEMP', 0)
        self.ui.ccdTemp.setText(f'{ccdTemp:4.1f}')

        expTime1 = header.get('EXPOSURE', 0)
        expTime2 = header.get('EXPTIME', 0)
        expTime = max(expTime1, expTime2)
        self.ui.expTime.setText(f'{expTime:5.1f}')

        filterCCD = header.get('FILTER', 0)
        self.ui.filter.setText(f'{filterCCD}')

        binX = header.get('XBINNING', 0)
        binY = header.get('YBINNING', 0)
        self.ui.binX.setText(f'{binX:1.0f}')
        self.ui.binY.setText(f'{binY:1.0f}')

        sqm = max(
            header.get('SQM', 0),
            header.get('SKY-QLTY', 0),
            header.get('MPSAS', 0),
        )
        self.ui.sqm.setText(f'{sqm:5.2f}')

        flipped = bool(header.get('FLIPPED', False))
        self.ui.isFlipped.setEnabled(flipped)

        # check if distortion is in header
        if 'CTYPE1' in header:
            wcsObject = wcs.WCS(header, relax=True)
            hasCelestial = wcsObject.has_celestial
            hasDistortion = wcsObject.has_distortion
        else:
            wcsObject = None
            hasCelestial = False
            hasDistortion = False

        self.ui.hasDistortion.setEnabled(hasDistortion)
        self.ui.hasWCS.setEnabled(hasCelestial)

        return hasDistortion, wcsObject

    def zoomImage(self, image=None, wcsObject=None):
        """
        zoomImage cutouts a portion of the original image to zoom in the image itself.
        it returns a copy of the image with an updated wcs content. we have to be careful
        about the use of Cutout2D, because they are mixing x and y coordinates. so position
        is in (x, y), but size ind in (y, x)

        :param image:
        :param wcsObject:
        :return:
        """

        if image is None:
            return None

        sizeY, sizeX = image.shape
        factor = self.zoomLevel[self.ui.zoom.currentText()]
        position = (int(sizeX / 2), int(sizeY / 2))
        size = (int(sizeY / factor), int(sizeX / factor))

        cutout = Cutout2D(
            image,
            position=position,
            size=size,
            wcs=wcsObject,
            copy=True,
        )

        return cutout.data

    def stretchImage(self, image=None):
        """
        stretchImage take the actual image and calculated norm based on the min, max
        derived from interval which is calculated with AsymmetricPercentileInterval.

        :param image: image
        :return: norm for plot
        """

        if image is None:
            return None

        values = self.stretchValues[self.ui.stretch.currentText()]
        interval = AsymmetricPercentileInterval(*values)
        vmin, vmax = interval.get_limits(image)
        # cutout the noise
        delta = vmax - vmin
        vmin = min(vmin + delta * 0.01, vmax)

        norm = ImageNormalize(
            image,
            vmin=vmin,
            vmax=vmax,
            stretch=SqrtStretch(),
        )

        return norm, vmin, vmax

    def colorImage(self):
        """
        colorImage take the index from gui and generates the colormap for image show
        command from matplotlib

        :return: color map
        """

        colorMap = self.colorMaps[self.ui.color.currentText()]

        return colorMap

    def setupDistorted(self, figure=None, wcsObject=None):
        """
        setupDistorted tries to setup all necessary context for displaying the image with
        wcs distorted coordinates.
        still plenty of work to be done, because very often the labels are not shown

        :param figure:
        :param wcsObject:
        :return: axes object to plot onto
        """

        if figure is None:
            return False
        if wcsObject is None:
            return False

        figure.clf()
        figure.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)

        axe = figure.add_subplot(1, 1, 1, projection=wcsObject, facecolor=None)
        axe.coords.frame.set_color(self.M_BLUE)

        axe0 = axe.coords[0]
        axe1 = axe.coords[1]
        if self.ui.checkShowGrid.isChecked():
            axe0.grid(True, color=self.M_BLUE, ls='solid', alpha=0.5)
            axe1.grid(True, color=self.M_BLUE, ls='solid', alpha=0.5)
        axe0.tick_params(colors=self.M_BLUE, labelsize=12)
        axe1.tick_params(colors=self.M_BLUE, labelsize=12)
        axe0.set_axislabel('Right Ascension',
                           color=self.M_BLUE,
                           fontsize=12,
                           fontweight='bold')
        axe1.set_axislabel('Declination',
                           color=self.M_BLUE,
                           fontsize=12,
                           fontweight='bold')
        axe0.set_ticks(number=10)
        axe1.set_ticks(number=10)
        axe0.set_ticks_position('lr')
        axe0.set_ticklabel_position('lr')
        axe0.set_axislabel_position('lr')
        axe1.set_ticks_position('tb')
        axe1.set_ticklabel_position('tb')
        axe1.set_axislabel_position('tb')

        return figure, axe

    def setupNormal(self, figure=None, header=None):
        """
        setupNormal build the image widget to show it with pixels as axes. the center of
        the image will have coordinates 0,0.

        :param figure:
        :param header:
        :return: axes object to plot onto
        """

        if figure is None:
            return False
        if header is None:
            return False

        figure.clf()
        # figure.subplots_adjust(left=0.1, right=0.95, bottom=0.1, top=0.95)

        axe = figure.add_subplot(1, 1, 1, facecolor=None)

        axe.tick_params(axis='x',
                        which='major',
                        colors=self.M_BLUE,
                        labelsize=12)
        axe.tick_params(axis='y',
                        which='major',
                        colors=self.M_BLUE,
                        labelsize=12)

        factor = self.zoomLevel[self.ui.zoom.currentText()]
        sizeX = header.get('NAXIS1', 1) / factor
        sizeY = header.get('NAXIS2', 1) / factor
        midX = int(sizeX / 2)
        midY = int(sizeY / 2)
        number = 10

        valueX, _ = np.linspace(-midX, midX, num=number, retstep=True)
        textX = list((str(int(x)) for x in valueX))
        ticksX = list((x + midX for x in valueX))
        axe.set_xticklabels(textX)
        axe.set_xticks(ticksX)

        valueY, _ = np.linspace(-midY, midY, num=number, retstep=True)
        textY = list((str(int(x)) for x in valueY))
        ticksY = list((x + midY for x in valueY))
        axe.set_yticklabels(textY)
        axe.set_yticks(ticksY)

        if self.ui.checkShowCrosshair.isChecked():
            axe.axvline(midX, color=self.M_RED)
            axe.axhline(midY, color=self.M_RED)
        if self.ui.checkShowGrid.isChecked():
            axe.grid(True, color=self.M_BLUE, ls='solid', alpha=0.5)

        axe.set_xlabel(xlabel='Pixel',
                       color=self.M_BLUE,
                       fontsize=12,
                       fontweight='bold')
        axe.set_ylabel(ylabel='Pixel',
                       color=self.M_BLUE,
                       fontsize=12,
                       fontweight='bold')
        return figure, axe

    def stackImages(self, imageData=None, header=None):
        """

        :param imageData:
        :param header: is only used, when stacking with alignment
        :return:
        """

        if np.shape(imageData) != np.shape(self.imageStack):
            self.imageStack = None

        # if first image, we just store the data as reference frame
        if self.imageStack is None:
            self.imageStack = imageData
            self.numberStack = 1
            return imageData

        # now we are going to stack the results
        self.numberStack += 1
        self.imageStack = np.add(self.imageStack, imageData)
        return self.imageStack / self.numberStack

    def showImage(self, imagePath=''):
        """
        showImage shows the fits image. therefore it calculates color map, stretch,
        zoom and other topics.

        :param imagePath:
        :return: success
        """

        if not imagePath:
            return False
        if not os.path.isfile(imagePath):
            return False

        self.imageFileName = imagePath
        full, short, ext = self.extractNames([imagePath])
        self.ui.imageFileName.setText(short)

        with fits.open(imagePath, mode='update') as fitsHandle:
            imageData = fitsHandle[0].data
            header = fitsHandle[0].header

        # check the bayer options, i normally us only RGGB pattern
        # todo: if it's an exposure directly, I get a bayer mosaic ??
        if 'BAYERPAT' in header and len(imageData.shape) > 2:
            imageData = cv2.cvtColor(imageData, cv2.COLOR_BAYER_BG2GRAY)

        # correct faulty headers, because some imaging programs did not
        # interpret the Keywords in the right manner (SGPro)
        if header.get('CTYPE1', '').endswith('DEF'):
            header['CTYPE1'] = header['CTYPE1'].replace('DEF', 'TAN')
        if header.get('CTYPE2', '').endswith('DEF'):
            header['CTYPE2'] = header['CTYPE2'].replace('DEF', 'TAN')

        if self.ui.checkStackImages.isChecked():
            imageData = self.stackImages(imageData=imageData, header=header)
            self.ui.numberStacks.setText(f'mean of: {self.numberStack:4.0f}')
        else:
            self.imageStack = None
            self.ui.numberStacks.setText('single')

        # check the data content and capabilities
        hasDistortion, wcsObject = self.writeHeaderToGUI(header=header)

        # process the image for viewing: stretching
        imageData = self.zoomImage(image=imageData, wcsObject=wcsObject)

        # normalization
        norm, iMin, iMax = self.stretchImage(image=imageData)

        # we process a colormap if we have a greyscale image
        colorMap = self.colorImage()

        # check the data content and capabilities
        useWCS = self.ui.checkUseWCS.isChecked()

        # check which type of presentation we would like to have
        if hasDistortion and useWCS:
            fig, axe = self.setupDistorted(figure=self.imageMat.figure,
                                           wcsObject=wcsObject)
        else:
            fig, axe = self.setupNormal(figure=self.imageMat.figure,
                                        header=header)

        # finally show it
        axe.imshow(imageData, norm=norm, cmap=colorMap, origin='lower')
        axe.figure.canvas.draw()

        return True

    def showCurrent(self):
        """

        :return: true for test purpose
        """
        self.showImage(self.imageFileName)
        return True

    def exposeRaw(self):
        """
        exposeImage gathers all necessary parameters and starts exposing

        :return: True for test purpose
        """

        expTime = self.app.mainW.ui.expTime.value()
        binning = self.app.mainW.ui.binning.value()
        subFrame = self.app.mainW.ui.subFrame.value()
        fastReadout = self.app.mainW.ui.checkFastDownload.isChecked()

        time = self.app.mount.obsSite.timeJD.utc_strftime('%Y-%m-%d-%H-%M-%S')
        fileName = time + '-exposure.fits'
        imagePath = self.app.mwGlob['imageDir'] + '/' + fileName

        self.imageFileNameOld = self.imageFileName

        self.app.camera.expose(
            imagePath=imagePath,
            expTime=expTime,
            binning=binning,
            subFrame=subFrame,
            fastReadout=fastReadout,
        )

        self.app.message.emit(f'Exposing: [{os.path.basename(imagePath)}]', 0)
        text = f'Duration: {expTime:3.0f}s  Bin: {binning:1.0f}  Sub: {subFrame:3.0f}%'
        self.app.message.emit(f'     {text}', 0)

        return True

    def exposeImageDone(self, imagePath=''):
        """
        exposeImageDone is the partner method to exposeImage. it resets the gui elements
        to it's default state and disconnects the signal for the callback. finally when
        all elements are done it emits the showImage signal.

        :param imagePath:
        :return: True for test purpose
        """

        self.deviceStat['expose'] = False
        self.app.camera.signals.saved.disconnect(self.exposeImageDone)
        self.app.message.emit(f'Exposed: [{os.path.basename(imagePath)}]', 0)

        if self.ui.checkAutoSolve.isChecked():
            self.signals.solveImage.emit(imagePath)
        else:
            self.signals.showImage.emit(imagePath)

        return True

    def exposeImage(self):
        """
        exposeImage disables all gui elements which could interfere when having a running
        exposure. it connects the callback for downloaded image and presets all necessary
        parameters for imaging

        :return: success
        """

        self.imageStack = None
        self.deviceStat['expose'] = True
        self.ui.checkStackImages.setChecked(False)
        self.app.camera.signals.saved.connect(self.exposeImageDone)
        self.exposeRaw()

        return True

    def exposeImageNDone(self, imagePath=''):
        """
        exposeImageNDone is the partner method to exposeImage. it resets the gui elements
        to it's default state and disconnects the signal for the callback. finally when
        all elements are done it emits the showImage signal.

        :param imagePath:
        :return: True for test purpose
        """

        self.app.message.emit(f'Exposed: [{os.path.basename(imagePath)}]', 0)

        if self.ui.checkAutoSolve.isChecked():
            self.signals.solveImage.emit(imagePath)
        else:
            self.signals.showImage.emit(imagePath)

        self.exposeRaw()

        return True

    def exposeImageN(self):
        """
        exposeImageN disables all gui elements which could interfere when having a running
        exposure. it connects the callback for downloaded image and presets all necessary
        parameters for imaging

        :return: success
        """

        self.imageStack = None
        self.deviceStat['exposeN'] = True
        self.app.camera.signals.saved.connect(self.exposeImageNDone)
        self.exposeRaw()

        return True

    def abortImage(self):
        """
        abortImage stops the exposure and resets the gui and the callback signals to default
        values

        :return: True for test purpose
        """

        self.app.camera.abort()

        # for disconnection we have to split which slots were connected to disable the
        # right ones
        if self.deviceStat['expose']:
            self.app.camera.signals.saved.disconnect(self.exposeImageDone)
        if self.deviceStat['exposeN']:
            self.app.camera.signals.saved.disconnect(self.exposeImageNDone)

        # last image file was nor stored, so getting last valid it back
        self.imageFileName = self.imageFileNameOld
        self.deviceStat['expose'] = False
        self.deviceStat['exposeN'] = False
        self.app.message.emit('Exposing aborted', 2)

        return True

    def solveDone(self, result=None):
        """
        solveDone is the partner method for solveImage. it enables the gui elements back
        removes the signal / slot connection for receiving solving results, checks the
        solving result itself and emits messages about the result. if solving succeeded,
        solveDone will redraw the image in the image window.

        :param result: result (named tuple)
        :return: success
        """

        self.deviceStat['solve'] = False
        self.app.astrometry.signals.done.disconnect(self.solveDone)

        if not result:
            self.app.message.emit('Solving error, result missing', 2)
            return False

        if result['success']:
            text = f'Solved : '
            text += f'Ra: {transform.convertToHMS(result["raJ2000S"])} '
            text += f'({result["raJ2000S"].hours:4.3f}), '
            text += f'Dec: {transform.convertToDMS(result["decJ2000S"])} '
            text += f'({result["decJ2000S"].degrees:4.3f}), '
            self.app.message.emit(text, 0)
            text = f'         '
            text += f'Angle: {result["angleS"]:3.0f}, '
            text += f'Scale: {result["scaleS"]:4.3f}, '
            text += f'Error: {result["errorRMS_S"]:4.1f}'
            self.app.message.emit(text, 0)
        else:
            text = f'Solving error: {result.get("message")}'
            self.app.message.emit(text, 2)
            return False

        isStack = self.ui.checkStackImages.isChecked()
        isAutoSolve = self.ui.checkAutoSolve.isChecked()
        if not isStack or isAutoSolve:
            self.signals.showImage.emit(result['solvedPath'])

        return True

    def solveImage(self, imagePath=''):
        """
        solveImage calls astrometry for solving th actual image in a threading manner.
        as result the gui will be active while the solving process takes part a
        background task. if the check update fits is selected the solution will be
        stored in the image header as well.
        solveImage will disable gui elements which might interfere when doing solve
        in background and sets the signal / slot connection for receiving the signal
        for finishing. this is linked to a second method solveDone, which is basically
        the partner method for handling this async behaviour of the gui.
        finally it emit a message about the start of solving

        :param imagePath:
        :return:
        """

        if not imagePath:
            return False
        if not os.path.isfile(imagePath):
            return False

        updateFits = self.ui.checkEmbedData.isChecked()
        solveTimeout = self.app.mainW.ui.solveTimeout.value()
        searchRadius = self.app.mainW.ui.searchRadius.value()
        self.app.astrometry.solveThreading(
            fitsPath=imagePath,
            radius=searchRadius,
            timeout=solveTimeout,
            updateFits=updateFits,
        )
        self.deviceStat['solve'] = True
        self.app.astrometry.signals.done.connect(self.solveDone)
        self.app.message.emit(f'Solving: [{os.path.basename(imagePath)}]', 0)

        return True

    def solveCurrent(self):
        """

        :return: true for test purpose
        """

        self.signals.solveImage.emit(self.imageFileName)
        return True

    def abortSolve(self):
        """
        abortSolve stops the exposure and resets the gui and the callback signals to default
        values

        :return: success
        """
        suc = self.app.astrometry.abort()

        return suc
Exemplo n.º 23
0
class PegasusUPB:

    __all__ = [
        'PegasusUPB',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):

        self.app = app
        self.threadPool = app.threadPool
        self.signals = PegasusUPBSignals()

        self.data = {}
        self.framework = None
        self.run = {
            'indi': PegasusUPBIndi(self.app, self.signals, self.data),
        }
        self.name = ''

        self.host = ('localhost', 7624)
        self.isGeometry = False

        # signalling from subclasses to main
        self.run['indi'].client.signals.serverConnected.connect(
            self.signals.serverConnected)
        self.run['indi'].client.signals.serverDisconnected.connect(
            self.signals.serverDisconnected)
        self.run['indi'].client.signals.deviceConnected.connect(
            self.signals.deviceConnected)
        self.run['indi'].client.signals.deviceDisconnected.connect(
            self.signals.deviceDisconnected)

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, value):
        self._host = value
        if self.framework in self.run.keys():
            self.run[self.framework].host = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        if self.framework in self.run.keys():
            self.run[self.framework].name = value

    def startCommunication(self):
        """

        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].startCommunication()
            return suc
        else:
            return False

    def stopCommunication(self):
        """

        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].stopCommunication()
            return suc
        else:
            return False

    def togglePowerPort(self, port=None):
        """
        togglePowerPort

        :param port:
        :return: true fot test purpose
        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].togglePowerPort(port=port)
            return suc
        else:
            return False

    def togglePowerPortBoot(self, port=None):
        """
        togglePowerPortBoot

        :param port:
        :return: true fot test purpose
        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].togglePowerPortBoot(port=port)
            return suc
        else:
            return False

    def toggleHubUSB(self):
        """
        toggleHubUSB

        :return: true fot test purpose
        """
        if self.framework in self.run.keys():
            suc = self.run[self.framework].toggleHubUSB()
            return suc
        else:
            return False

    def togglePortUSB(self, port=None):
        """
        togglePortUSB

        :param port:
        :return: true fot test purpose
        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].togglePortUSB(port=port)
            return suc
        else:
            return False

    def toggleAutoDew(self):
        """
        toggleAutoDewPort

        :return: true fot test purpose
        """
        if self.framework in self.run.keys():
            suc = self.run[self.framework].toggleAutoDew()
            return suc
        else:
            return False
        return suc

    def toggleAutoDewPort(self, port=None):
        """
        toggleAutoDewPort

        :param port:
        :return: true fot test purpose
        """
        if self.framework in self.run.keys():
            suc = self.run[self.framework].toggleAutoDewPort(port=port)
            return suc
        else:
            return False
        return suc

    def sendDew(self, port='', value=None):
        """

        :param port:
        :param value:
        :return: success
        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].sendDew(port=port, value=value)
            return suc
        else:
            return False

    def sendAdjustableOutput(self, value=None):
        """

        :param value:
        :return: success
        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].sendAdjustableOutput(value=value)
            return suc
        else:
            return False
Exemplo n.º 24
0
class SensorWeatherIndi(IndiClass):
    """
    the class SensorWeatherIndi inherits all information and handling of the SensorWeather device

        >>> SensorWeatherIndi(host=None,
        >>>         name=''
        >>>         )
    """

    __all__ = [
        'SensorWeatherIndi',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1 seconds for setting indi server
    UPDATE_RATE = 1

    def __init__(self, app=None, signals=None, data=None):
        super().__init__(app=app, data=data)

        self.signals = signals
        self.data = data

    def setUpdateConfig(self, deviceName):
        """
        setUpdateRate corrects the update rate of weather devices to get an defined
        setting regardless, what is setup in server side.

        :param deviceName:
        :return: success
        """

        if deviceName != self.name:
            return False

        if self.device is None:
            return False

        update = self.device.getNumber('WEATHER_UPDATE')

        if 'PERIOD' not in update:
            return False

        if update.get('PERIOD', 0) == self.UPDATE_RATE:
            return True

        update['PERIOD'] = self.UPDATE_RATE
        suc = self.client.sendNewNumber(deviceName=deviceName,
                                        propertyName='WEATHER_UPDATE',
                                        elements=update)
        return suc

    def updateNumber(self, deviceName, propertyName):
        """
        updateNumber is called whenever a new number is received in client. it runs
        through the device list and writes the number data to the according locations.
        for global weather data as there is no dew point value available, it calculates
        it and stores it as value as well.

        if no dew point is available in data, it will calculate this value from
        temperature and humidity.

        :param deviceName:
        :param propertyName:
        :return:
        """

        if self.device is None:
            return False
        if deviceName != self.name:
            return False

        for element, value in self.device.getNumber(propertyName).items():
            # consolidate to WEATHER_PRESSURE
            if element == 'WEATHER_BAROMETER':
                element = 'WEATHER_PRESSURE'

            key = propertyName + '.' + element
            self.data[key] = value

            # print(self.name, key, value)

        return True
Exemplo n.º 25
0
class Dome:

    __all__ = [
        'Dome',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(self, app):

        self.app = app
        self.threadPool = app.threadPool
        self.signals = DomeSignals()

        self.data = {}
        self.framework = None
        self.run = {
            'indi': DomeIndi(self.app, self.signals, self.data),
            'alpaca': DomeAlpaca(self.app, self.signals, self.data),
        }
        self.name = ''

        self.host = ('localhost', 7624)
        self.isGeometry = False

        # signalling from subclasses to main
        alpacaSignals = self.run['alpaca'].client.signals
        alpacaSignals.serverConnected.connect(self.signals.serverConnected)
        alpacaSignals.serverDisconnected.connect(
            self.signals.serverDisconnected)
        alpacaSignals.deviceConnected.connect(self.signals.deviceConnected)
        alpacaSignals.deviceDisconnected.connect(
            self.signals.deviceDisconnected)

        indiSignals = self.run['indi'].client.signals
        indiSignals.serverConnected.connect(self.signals.serverConnected)
        indiSignals.serverDisconnected.connect(self.signals.serverDisconnected)
        indiSignals.deviceConnected.connect(self.signals.deviceConnected)
        indiSignals.deviceDisconnected.connect(self.signals.deviceDisconnected)

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, value):
        self._host = value
        if self.framework in self.run.keys():
            self.run[self.framework].host = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        if self.framework in self.run.keys():
            self.run[self.framework].name = value

    @property
    def settlingTime(self):
        if self.framework in self.run.keys():
            return self.run[self.framework].settlingTime
        else:
            return None

    @settlingTime.setter
    def settlingTime(self, value):
        if self.framework in self.run.keys():
            self.run[self.framework].settlingTime = value

    def startCommunication(self):
        """

        """

        if self.framework in self.run.keys():
            suc = self.run[self.framework].startCommunication()
            return suc
        else:
            return False

    def stopCommunication(self):
        """

        """

        if self.framework in self.run.keys():
            self.signals.message.emit('')
            suc = self.run[self.framework].stopCommunication()
            return suc
        else:
            return False

    def slewDome(self, altitude=0, azimuth=0):
        """

        :param altitude:
        :param azimuth:
        :return: success
        """

        if not self.data:
            return False

        if self.isGeometry:
            ha = self.app.mount.obsSite.haJNowTarget.radians
            dec = self.app.mount.obsSite.decJNowTarget.radians
            lat = self.app.mount.obsSite.location.latitude.radians
            pierside = self.app.mount.obsSite.piersideTarget
            alt, az = self.app.mount.geometry.calcTransformationMatrices(
                ha=ha, dec=dec, lat=lat, pierside=pierside)
            alt = alt.degrees
            az = az.degrees

            # todo: correct calculation that this is not necessary
            if alt is np.nan or az is np.nan:
                self.log.warning(
                    f'alt:{altitude}, az:{azimuth}, pier:{pierside}')

            if alt is np.nan:
                alt = altitude
            if az is np.nan:
                az = azimuth

        else:
            alt = altitude
            az = azimuth

        geoStat = 'Geometry corrected' if self.isGeometry else 'Equal mount'
        delta = azimuth - az
        text = f'Slewing  dome:       {geoStat}, az: {az:3.1f} delta: {delta:3.1f}°'
        self.app.message.emit(text, 0)

        suc = self.run[self.framework].slewToAltAz(azimuth=az, altitude=alt)

        return suc
Exemplo n.º 26
0
class Hipparcos(object):
    """
    The class Data inherits all information and handling of hipparcos data and other
    attributes. this includes data about the alignment stars defined in generateAlignStars,
    their ra dec coordinates, proper motion, parallax and radial velocity and the
    calculation of data for display and slew commands

        >>> hip = Hipparcos(
        >>>                 app=app
        >>>                 )
    """

    __all__ = [
        'Hipparcos',
        'calculateAlignStarsPositionsAltAz',
        'getAlignStarRaDecFromIndex',
    ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    def __init__(
        self,
        app=None,
    ):

        self.app = app
        self.lat = app.mount.obsSite.location.latitude.degrees
        self.name = list()
        self.alt = list()
        self.az = list()
        self.alignStars = generateAlignStars()
        self.calculateAlignStarPositionsAltAz()

    def calculateAlignStarPositionsAltAz(self):
        """
        calculateAlignStarPositionsAltAz does calculate the star coordinates from give data
        out of generated star list. calculation routines are from astropy erfa. atco13 does
        the results based on proper motion, parallax and radial velocity and need J2000
        coordinates. because of using the hipparcos catalogue, which is based on J1991,
        25 epoch the pre calculation from J1991,25 to J2000 is done already when generating
        the alignstars file. there is no refraction data taken into account, because we need
        this only for display purpose and for this, the accuracy is more than sufficient.

        :return: lists for alt, az and name of star
        """

        location = self.app.mount.obsSite.location
        if location is None:
            return False
        t = self.app.mount.obsSite.timeJD
        star = list(self.alignStars.values())
        self.name = list(self.alignStars.keys())

        aob, zob, hob, dob, rob, eo = erfa.atco13(
            [x[0] for x in star], [x[1] for x in star], [x[2] for x in star],
            [x[3] for x in star], [x[4] for x in star], [x[5]
                                                         for x in star], t.ut1,
            0.0, t.dut1, location.longitude.radians, location.latitude.radians,
            location.elevation.m, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
        self.az = aob * 360 / 2 / np.pi
        self.alt = 90.0 - zob * 360 / 2 / np.pi
        return True

    def getAlignStarRaDecFromName(self, name):
        """
        getAlignStarRaDecFromName does calculate the star coordinates from give data
        out of generated star list. calculation routines are from astropy erfa. atco13 does
        the results based on proper motion, parallax and radial velocity and need J2000
        coordinates. because of using the hipparcos catalogue, which is based on J1991,
        25 epoch the pre calculation from J1991,25 to J2000 is done already when generating
        the alignstars file. there is no refraction data taken into account, because we need
        this only for display purpose and for this, the accuracy is more than sufficient.

        the return values are in JNow epoch as the mount only handles this epoch !

        :param name: name of star
        :return: values for ra, dec in hours / degrees in JNow epoch !
        """

        if name not in self.alignStars:
            return None, None
        t = self.app.mount.obsSite.ts.now()
        values = self.alignStars[name]

        ra, dec, eo = erfa.atci13(
            values[0],
            values[1],
            values[2],
            values[3],
            values[4],
            values[5],
            t.ut1,
            0.0,
        )
        ra = erfa.anp(ra - eo) * 24 / 2 / np.pi
        dec = dec * 360 / 2 / np.pi

        return ra, dec
Exemplo n.º 27
0
class MeasureData(object):
    """
    the class MeasureData inherits all information and handling of data management and
    storage

        >>> measure = MeasureData(
        >>>                 )
    """

    __all__ = ['MeasureData',
               'startMeasurement',
               'stopMeasurement',
               ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # update rate to 1 seconds for setting indi server
    CYCLE_UPDATE_TASK = 1000
    # maximum size of measurement task
    MAXSIZE = 24 * 60 * 60

    def __init__(self, app):

        self.app = app
        self.mutexMeasure = PyQt5.QtCore.QMutex()

        # internal calculations
        self.shorteningStart = True
        self.raRef = None
        self.decRef = None

        self.data = {}
        self.devices = {}
        self.deviceStat = None

        # minimum set for driver package built in
        self.name = ''
        self.framework = None
        self.run = {
            'built-in': self
        }

        # time for measurement
        self.timerTask = PyQt5.QtCore.QTimer()
        self.timerTask.setSingleShot(False)
        self.timerTask.timeout.connect(self.measureTask)

    def startCommunication(self):
        """

        :return: True for test purpose
        """

        dItems = self.deviceStat.items()
        self.devices = [key for key, value in dItems if self.deviceStat[key] is not None]
        self.setEmptyData()
        self.timerTask.start(self.CYCLE_UPDATE_TASK)

        return True

    def stopCommunication(self):
        """

        :return: True for test purpose
        """

        self.timerTask.stop()
        return True

    def setEmptyData(self):
        """

        :return: True for test purpose
        """

        self.data = {
            'time': np.empty(shape=[0, 1], dtype='datetime64'),
            'raJNow': np.empty(shape=[0, 1]),
            'decJNow': np.empty(shape=[0, 1]),
            'status': np.empty(shape=[0, 1]),
        }

        if 'sensorWeather' in self.devices:
            self.data['sensorWeatherTemp'] = np.empty(shape=[0, 1])
            self.data['sensorWeatherHum'] = np.empty(shape=[0, 1])
            self.data['sensorWeatherPress'] = np.empty(shape=[0, 1])
            self.data['sensorWeatherDew'] = np.empty(shape=[0, 1])

        if 'onlineWeather' in self.devices:
            self.data['onlineWeatherTemp'] = np.empty(shape=[0, 1])
            self.data['onlineWeatherHum'] = np.empty(shape=[0, 1])
            self.data['onlineWeatherPress'] = np.empty(shape=[0, 1])
            self.data['onlineWeatherDew'] = np.empty(shape=[0, 1])

        if 'directWeather' in self.devices:
            self.data['directWeatherTemp'] = np.empty(shape=[0, 1])
            self.data['directWeatherHum'] = np.empty(shape=[0, 1])
            self.data['directWeatherPress'] = np.empty(shape=[0, 1])
            self.data['directWeatherDew'] = np.empty(shape=[0, 1])

        if 'skymeter' in self.devices:
            self.data['skyTemp'] = np.empty(shape=[0, 1])
            self.data['skySQR'] = np.empty(shape=[0, 1])

        if 'filterwheel' in self.devices:
            self.data['filterNumber'] = np.empty(shape=[0, 1])

        if 'focuser' in self.devices:
            self.data['focusPosition'] = np.empty(shape=[0, 1])

        if 'power' in self.devices:
            self.data['powCurr1'] = np.empty(shape=[0, 1])
            self.data['powCurr2'] = np.empty(shape=[0, 1])
            self.data['powCurr3'] = np.empty(shape=[0, 1])
            self.data['powCurr4'] = np.empty(shape=[0, 1])
            self.data['powVolt'] = np.empty(shape=[0, 1])
            self.data['powCurr'] = np.empty(shape=[0, 1])
            self.data['powHum'] = np.empty(shape=[0, 1])
            self.data['powTemp'] = np.empty(shape=[0, 1])
            self.data['powDew'] = np.empty(shape=[0, 1])

        return True

    def calculateReference(self):
        """
        calculateReference run the states to get the calculation with references for
        RaDec deviations better stable. it takes into account, when the mount is tracking
        and when we calculate the offset (ref) to make the deviations balanced to zero

        :return: raJNow, decJNow
        """

        dat = self.data
        obs = self.app.mount.obsSite

        raJNow = 0
        decJNow = 0
        if obs.raJNow is None:
            return raJNow, decJNow

        length = len(dat['status'])
        period = min(length, 10)
        hasMean = length > 0 and period > 0

        if not hasMean:
            return raJNow, decJNow

        periodData = dat['status'][-period:]
        hasValidData = all(x is not None for x in periodData)

        if hasValidData:
            trackingIsStable = (periodData.mean() == 0)
        else:
            trackingIsStable = False

        if trackingIsStable:
            if self.raRef is None:
                self.raRef = obs.raJNow._degrees
            if self.decRef is None:
                self.decRef = obs.decJNow.degrees
            # we would like to have the difference in arcsec
            raJNow = (obs.raJNow._degrees - self.raRef) * 3600
            decJNow = (obs.decJNow.degrees - self.decRef) * 3600
        else:
            self.raRef = None
            self.decRef = None

        return raJNow, decJNow

    def checkStart(self, lenData):
        """
        checkStart throws the first N measurements away, because they or not valid

        :param lenData:
        :return: True if splitting happens
        """

        if self.shorteningStart and lenData > 2:
            self.shorteningStart = False
            for measure in self.data:
                self.data[measure] = np.delete(self.data[measure], range(0, 2))

    def checkSize(self, lenData):
        """
        checkSize keep tracking of memory usage of the measurement. if the measurement
        get s too much data, it split the history by half and only keeps the latest only
        for work.
        if as well throws the first N measurements away, because they or not valid

        :param lenData:
        :return: True if splitting happens
        """

        if lenData < self.MAXSIZE:
            return False

        for measure in self.data:
            self.data[measure] = np.split(self.data[measure], 2)[1]
        return True

    def getDirectWeather(self):
        """
        getDirectWeather checks if data is already collected and send 0 in case of missing
        data.

        :return: values
        """

        temp = self.app.mount.setting.weatherTemperature
        if temp is None:
            temp = 0
        press = self.app.mount.setting.weatherPressure
        if press is None:
            press = 0
        dew = self.app.mount.setting.weatherDewPoint
        if dew is None:
            dew = 0
        hum = self.app.mount.setting.weatherHumidity
        if hum is None:
            hum = 0

        return temp, press, dew, hum

    def measureTask(self):
        """
        measureTask runs all necessary pre processing and collecting task to assemble a
        large dict of lists, where all measurement data is stored. the intention later on
        would be to store and export this data.
        the time object is related to the time held in mount computer and is in utc timezone.

        data sources are:
            environment
            mount pointing position

        :return: success
        """

        if not self.mutexMeasure.tryLock():
            self.log.info('overrun in measure')
            return False

        lenData = len(self.data['time'])
        self.checkStart(lenData)
        self.checkSize(lenData)

        dat = self.data

        # gathering all the necessary data
        raJNow, decJNow = self.calculateReference()
        timeStamp = self.app.mount.obsSite.timeJD.utc_datetime().replace(tzinfo=None)
        dat['time'] = np.append(dat['time'], np.datetime64(timeStamp))
        dat['raJNow'] = np.append(dat['raJNow'], raJNow)
        dat['decJNow'] = np.append(dat['decJNow'], decJNow)
        dat['status'] = np.append(dat['status'], self.app.mount.obsSite.status)

        if 'sensorWeather' in self.devices:
            sens = self.app.sensorWeather
            sensorWeatherTemp = sens.data.get('WEATHER_PARAMETERS.WEATHER_TEMPERATURE', 0)
            sensorWeatherPress = sens.data.get('WEATHER_PARAMETERS.WEATHER_PRESSURE', 0)
            sensorWeatherDew = sens.data.get('WEATHER_PARAMETERS.WEATHER_DEWPOINT', 0)
            sensorWeatherHum = sens.data.get('WEATHER_PARAMETERS.WEATHER_HUMIDITY', 0)
            dat['sensorWeatherTemp'] = np.append(dat['sensorWeatherTemp'], sensorWeatherTemp)
            dat['sensorWeatherHum'] = np.append(dat['sensorWeatherHum'], sensorWeatherHum)
            dat['sensorWeatherPress'] = np.append(dat['sensorWeatherPress'], sensorWeatherPress)
            dat['sensorWeatherDew'] = np.append(dat['sensorWeatherDew'], sensorWeatherDew)

        if 'onlineWeather' in self.devices:
            onlineWeatherTemp = self.app.onlineWeather.data.get('temperature', 0)
            onlineWeatherPress = self.app.onlineWeather.data.get('pressure', 0)
            onlineWeatherDew = self.app.onlineWeather.data.get('dewPoint', 0)
            onlineWeatherHum = self.app.onlineWeather.data.get('humidity', 0)
            dat['onlineWeatherTemp'] = np.append(dat['onlineWeatherTemp'], onlineWeatherTemp)
            dat['onlineWeatherHum'] = np.append(dat['onlineWeatherHum'], onlineWeatherHum)
            dat['onlineWeatherPress'] = np.append(dat['onlineWeatherPress'], onlineWeatherPress)
            dat['onlineWeatherDew'] = np.append(dat['onlineWeatherDew'], onlineWeatherDew)

        if 'directWeather' in self.devices:
            temp, press, dew, hum = self.getDirectWeather()
            dat['directWeatherTemp'] = np.append(dat['directWeatherTemp'], temp)
            dat['directWeatherHum'] = np.append(dat['directWeatherHum'], hum)
            dat['directWeatherPress'] = np.append(dat['directWeatherPress'], press)
            dat['directWeatherDew'] = np.append(dat['directWeatherDew'], dew)

        if 'skymeter' in self.devices:
            skySQR = self.app.skymeter.data.get('SKY_QUALITY.SKY_BRIGHTNESS', 0)
            skyTemp = self.app.skymeter.data.get('SKY_QUALITY.SKY_TEMPERATURE', 0)
            dat['skySQR'] = np.append(dat['skySQR'], skySQR)
            dat['skyTemp'] = np.append(dat['skyTemp'], skyTemp)

        if 'filterwheel' in self.devices:
            filterNo = self.app.filterwheel.data.get('FILTER_SLOT.FILTER_SLOT_VALUE', 0)
            dat['filterNumber'] = np.append(dat['filterNumber'], filterNo)

        if 'focuser' in self.devices:
            focus = self.app.focuser.data.get('ABS_FOCUS_POSITION.FOCUS_ABSOLUTE_POSITION', 0)
            dat['focusPosition'] = np.append(dat['focusPosition'], focus)

        if 'power' in self.devices:
            powCurr1 = self.app.power.data.get('POWER_CURRENT.POWER_CURRENT_1', 0)
            powCurr2 = self.app.power.data.get('POWER_CURRENT.POWER_CURRENT_2', 0)
            powCurr3 = self.app.power.data.get('POWER_CURRENT.POWER_CURRENT_3', 0)
            powCurr4 = self.app.power.data.get('POWER_CURRENT.POWER_CURRENT_4', 0)
            powVolt = self.app.power.data.get('POWER_SENSORS.SENSOR_VOLTAGE', 0)
            powCurr = self.app.power.data.get('POWER_SENSORS.SENSOR_CURRENT', 0)
            powTemp = self.app.power.data.get('WEATHER_PARAMETERS.WEATHER_TEMPERATURE', 0)
            powDew = self.app.power.data.get('WEATHER_PARAMETERS.WEATHER_DEWPOINT', 0)
            powHum = self.app.power.data.get('WEATHER_PARAMETERS.WEATHER_HUMIDITY', 0)
            dat['powCurr1'] = np.append(dat['powCurr1'], powCurr1)
            dat['powCurr2'] = np.append(dat['powCurr2'], powCurr2)
            dat['powCurr3'] = np.append(dat['powCurr3'], powCurr3)
            dat['powCurr4'] = np.append(dat['powCurr4'], powCurr4)
            dat['powCurr'] = np.append(dat['powCurr'], powCurr)
            dat['powVolt'] = np.append(dat['powVolt'], powVolt)
            dat['powTemp'] = np.append(dat['powTemp'], powTemp)
            dat['powDew'] = np.append(dat['powDew'], powDew)
            dat['powHum'] = np.append(dat['powHum'], powHum)

        self.mutexMeasure.unlock()
        return True
Exemplo n.º 28
0
class AlpacaClass(object):
    """
    the class AlpacaClass inherits all information and handling of alpaca devices
    this class will be only referenced from other classes and not directly used

        >>> a = AlpacaClass(app=None, data={})
    """

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # relaxed generic timing
    CYCLE_DEVICE = 3000
    CYCLE_DATA = 3000

    def __init__(self, app=None, data={}):
        super().__init__()

        self.app = app
        self.threadPool = app.threadPool

        self.client = AlpacaBase()
        self.data = data

        self.deviceConnected = False
        self.serverConnected = False

        self.cycleDevice = PyQt5.QtCore.QTimer()
        self.cycleDevice.setSingleShot(False)
        self.cycleDevice.timeout.connect(self.startPollStatus)

        self.cycleData = PyQt5.QtCore.QTimer()
        self.cycleData.setSingleShot(False)
        self.cycleData.timeout.connect(self.pollData)

    @property
    def host(self):
        return self.client.host

    @host.setter
    def host(self, value):
        self.client.host = value

    @property
    def name(self):
        return self.client.name

    @name.setter
    def name(self, value):
        self.client.name = value

    @property
    def apiVersion(self):
        return self.client.apiVersion

    @apiVersion.setter
    def apiVersion(self, value):
        self.client.apiVersion = value

    @property
    def protocol(self):
        return self.client.protocol

    @protocol.setter
    def protocol(self, value):
        self.client.protocol = value

    def getInitialConfig(self):
        """

        :return: success of reconnecting to server
        """
        self.client.connected(Connected=True)
        suc = self.client.connected()
        if not suc:
            return False

        if not self.serverConnected:
            self.serverConnected = True
            self.client.signals.serverConnected.emit()

        if not self.deviceConnected:
            self.deviceConnected = True
            self.client.signals.deviceConnected.emit(f'{self.name}')
            self.app.message.emit(f'Alpaca device found: [{self.name}]', 0)

        self.data['DRIVER_INFO.DRIVER_NAME'] = self.client.nameDevice()
        self.data['DRIVER_INFO.DRIVER_VERSION'] = self.client.driverVersion()
        self.data['DRIVER_INFO.DRIVER_EXEC'] = self.client.driverInfo()

        return True

    def startTimer(self):
        """
        startTimer enables the cyclic timer for polling information

        :return: true for test purpose
        """
        self.cycleData.start(self.CYCLE_DATA)
        self.cycleDevice.start(self.CYCLE_DEVICE)
        return True

    def stopTimer(self):
        """
        stopTimer disables the cyclic timer for polling information

        :return: true for test purpose
        """
        self.cycleData.stop()
        self.cycleDevice.stop()
        return True

    def dataEntry(self, value, element, elementInv=None):
        """

        :param value:
        :param element:
        :param elementInv:
        :return: reset entry
        """

        resetValue = value is None and element in self.data
        if resetValue:
            del self.data[element]
        else:
            self.data[element] = value

        if elementInv is None:
            return resetValue

        resetValue = value is None and elementInv in self.data
        if resetValue:
            del self.data[elementInv]
        else:
            self.data[elementInv] = value

        return resetValue

    def pollStatus(self):
        """
        pollStatus is the thread method to be called for collecting data

        :return: success
        """

        suc = self.client.connected()
        if self.deviceConnected and not suc:
            self.deviceConnected = False
            self.client.signals.deviceDisconnected.emit(f'{self.name}')
            self.app.message.emit(f'Alpaca device remove:[{self.name}]', 0)

        elif not self.deviceConnected and suc:
            self.deviceConnected = True
            self.client.signals.deviceConnected.emit(f'{self.name}')
            self.app.message.emit(f'Alpaca device found: [{self.name}]', 0)

        else:
            pass

        return suc

    def emitData(self):
        pass

    def workerPollData(self):
        pass

    def pollData(self):
        """

        :return: success
        """

        if not self.deviceConnected:
            return False

        worker = Worker(self.workerPollData)
        worker.signals.result.connect(self.emitData)
        self.threadPool.start(worker)
        return True

    def startPollStatus(self):
        """
        startPollStatus starts a thread every 1 second for polling.

        :return: success
        """
        worker = Worker(self.pollStatus)
        self.threadPool.start(worker)

        return True

    def startCommunication(self):
        """
        startCommunication starts cycling of the polling.

        :return: True for connecting to server
        """

        worker = Worker(self.getInitialConfig)
        worker.signals.finished.connect(self.startTimer)
        self.threadPool.start(worker)

        return True

    def stopCommunication(self):
        """
        stopCommunication stops cycling of the server.

        :return: true for test purpose
        """

        self.stopTimer()
        self.deviceConnected = False
        self.serverConnected = False
        self.client.signals.deviceDisconnected.emit(f'{self.name}')
        self.client.signals.serverDisconnected.emit({f'{self.name}': 0})
        self.app.message.emit(f'Alpaca device remove:[{self.name}]', 0)

        worker = Worker(self.client.connected, Connected=False)
        self.threadPool.start(worker)

        return True
Exemplo n.º 29
0
class SatelliteWindow(widget.MWidget):
    """
    the satellite window class handles

    """

    __all__ = ['SatelliteWindow',
               'receiveSatelliteAndShow',
               'updatePositions',
               ]

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})

    # length of forecast time in hours
    FORECAST_TIME = 3
    # earth radius
    EARTH_RADIUS = 6378.0

    def __init__(self, app, threadPool):
        super().__init__()
        self.app = app
        self.threadPool = threadPool

        self.ui = satellite_ui.Ui_SatelliteDialog()
        self.ui.setupUi(self)
        self.initUI()
        self.signals = SatelliteWindowSignals()
        self.satellite = None
        self.plotSatPosSphere1 = None
        self.plotSatPosSphere2 = None
        self.plotSatPosHorizon = None
        self.plotSatPosEarth = None

        self.satSphereMat1 = self.embedMatplot(self.ui.satSphere1)
        self.satSphereMat1.parentWidget().setStyleSheet(self.BACK_BG)
        self.satSphereMat2 = self.embedMatplot(self.ui.satSphere2)
        self.satSphereMat2.parentWidget().setStyleSheet(self.BACK_BG)
        self.satHorizonMat = self.embedMatplot(self.ui.satHorizon)
        self.satHorizonMat.parentWidget().setStyleSheet(self.BACK_BG)
        self.satEarthMat = self.embedMatplot(self.ui.satEarth)
        self.satEarthMat.parentWidget().setStyleSheet(self.BACK_BG)

        self.signals.show.connect(self.receiveSatelliteAndShow)
        self.signals.update.connect(self.updatePositions)

        # as we cannot access data from Qt resource system, we have to convert it to
        # ByteIO first
        stream = PyQt5.QtCore.QFile(':/data/worldmap.dat')
        stream.open(PyQt5.QtCore.QFile.ReadOnly)
        pickleData = stream.readAll()
        stream.close()
        # loading the world image from nasa as PNG as matplotlib only loads png.
        self.world = pickle.load(BytesIO(pickleData))

        self.initConfig()
        self.showWindow()

    def initConfig(self):
        """
        initConfig read the key out of the configuration dict and stores it to the gui
        elements. if some initialisations have to be proceeded with the loaded persistent
        data, they will be launched as well in this method.

        :return: True for test purpose
        """

        if 'satelliteW' not in self.app.config:
            self.app.config['satelliteW'] = {}
        config = self.app.config['satelliteW']
        x = config.get('winPosX', 100)
        y = config.get('winPosY', 100)
        if x > self.screenSizeX:
            x = 0
        if y > self.screenSizeY:
            y = 0
        self.move(x, y)
        height = config.get('height', 600)
        width = config.get('width', 800)
        self.resize(width, height)

        return True

    def storeConfig(self):
        """
        storeConfig writes the keys to the configuration dict and stores. if some
        saving has to be proceeded to persistent data, they will be launched as
        well in this method.

        :return: True for test purpose
        """
        if 'satelliteW' not in self.app.config:
            self.app.config['satelliteW'] = {}
        config = self.app.config['satelliteW']
        config['winPosX'] = self.pos().x()
        config['winPosY'] = self.pos().y()
        config['height'] = self.height()
        config['width'] = self.width()

        return True

    def closeEvent(self, closeEvent):
        """
        closeEvent is overloaded to be able to store the data before the windows is close
        and all it's data is garbage collected

        :param closeEvent:
        :return:
        """
        self.storeConfig()

        # gui signals
        super().closeEvent(closeEvent)

    def showWindow(self):
        """
        showWindow starts constructing the main window for satellite view and shows the
        window content

        :return: True for test purpose
        """
        self.receiveSatelliteAndShow(self.app.mainW.satellite)
        self.show()

        return True

    @staticmethod
    def markerSatellite():
        """
        markerSatellite constructs a custom marker for presentation of satellite view

        :return: marker
        """

        circle = mpath.Path.unit_circle()

        rect1p = [[0, 0], [1, 2], [0, 3], [4, 7], [7, 4], [3, 0], [2, 1], [6, 5], [5, 6],
                  [1, 2], [2, 1], [0, 0]]
        rect1p = np.array(rect1p)
        rect1c = [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 79]
        rect1c = np.array(rect1c)
        # concatenate the circle with an internal cutout of the star
        verts = np.concatenate([rect1p,
                                rect1p * -1,
                                circle.vertices])
        codes = np.concatenate([rect1c,
                                rect1c,
                                circle.codes])
        marker = mpath.Path(verts, codes)
        return marker

    def receiveSatelliteAndShow(self, satellite=None):
        """
        receiveSatelliteAndShow receives a signal with the content of the selected satellite.
        it locally sets it an draws the the complete view

        :param satellite:
        :return: true for test purpose
        """

        if satellite is None:
            self.drawSatellite()
            return False

        self.satellite = satellite
        if satellite.model.no < 0.02:
            self.FORECAST_TIME = 24
        else:
            self.FORECAST_TIME = 3
        self.drawSatellite()

        return True

    def updatePositions(self, observe=None, subpoint=None, altaz=None):
        """
        updatePositions is triggered once a second and update the satellite position in each
        view.

        :return: success
        """
        if observe is None:
            return False
        if subpoint is None:
            return False
        if altaz is None:
            return False

        if self.satellite is None:
            return False
        if self.plotSatPosEarth is None:
            return False
        if self.plotSatPosHorizon is None:
            return False
        if self.plotSatPosSphere1 is None:
            return False
        if self.plotSatPosSphere2 is None:
            return False

        # sphere1
        x, y, z = observe.position.km
        self.plotSatPosSphere1.set_data_3d((x, y, z))

        # sphere2
        lat = subpoint.latitude.radians
        lon = subpoint.longitude.radians
        elev = subpoint.elevation.m / 1000 + self.EARTH_RADIUS
        x, y, z = transform.sphericalToCartesian(azimuth=lon,
                                                 altitude=lat,
                                                 radius=elev)
        self.plotSatPosSphere2.set_data_3d((x, y, z))

        # earth
        lat = subpoint.latitude.degrees
        lon = subpoint.longitude.degrees
        self.plotSatPosEarth.set_data((lon, lat))

        # horizon
        alt, az, _ = altaz
        alt = alt.degrees
        az = az.degrees
        self.plotSatPosHorizon.set_data((az, alt))

        # update the plot and redraw
        self.satSphereMat1.figure.canvas.draw()
        self.satEarthMat.figure.canvas.draw()
        self.satHorizonMat.figure.canvas.draw()

        return True

    @staticmethod
    def makeCubeLimits(axe, centers=None, hw=None):
        """

        :param axe:
        :param centers:
        :param hw:
        :return:
        """

        limits = axe.get_xlim(), axe.get_ylim(), axe.get_zlim()
        if centers is None:
            centers = [0.5 * sum(pair) for pair in limits]

        if hw is None:
            widths = [pair[1] - pair[0] for pair in limits]
            hw = 0.5 * max(widths)
            axe.set_xlim(centers[0] - hw, centers[0] + hw)
            axe.set_ylim(centers[1] - hw, centers[1] + hw)
            axe.set_zlim(centers[2] - hw, centers[2] + hw)

        else:
            try:
                hwx, hwy, hwz = hw
                axe.set_xlim(centers[0] - hwx, centers[0] + hwx)
                axe.set_ylim(centers[1] - hwy, centers[1] + hwy)
                axe.set_zlim(centers[2] - hwz, centers[2] + hwz)
            except Exception:
                axe.set_xlim(centers[0] - hw, centers[0] + hw)
                axe.set_ylim(centers[1] - hw, centers[1] + hw)
                axe.set_zlim(centers[2] - hw, centers[2] + hw)

        return centers, hw

    def drawSphere1(self, observe=None):
        """
        draw sphere and put face color als image overlay:

        https://stackoverflow.com/questions/53074908/
        map-an-image-onto-a-sphere-and-plot-3d-trajectories

        but performance problems

        see also:

        https://space.stackexchange.com/questions/25958/
        how-can-i-plot-a-satellites-orbit-in-3d-from-a-tle-using-python-and-skyfield

        :param observe:
        :return: success
        """

        figure = self.satSphereMat1.figure
        figure.clf()
        figure.subplots_adjust(left=-0.1, right=1.1, bottom=-0.3, top=1.2)
        axe = figure.add_subplot(111, projection='3d')

        # switching all visual grids and planes off
        axe.set_facecolor((0, 0, 0, 0))
        axe.tick_params(colors=self.M_GREY,
                        labelsize=12)
        axe.set_axis_off()
        axe.w_xaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))
        axe.w_yaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))
        axe.w_zaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))

        # calculating sphere
        theta = np.linspace(0, 2 * np.pi, 51)
        cth, sth, zth = [f(theta) for f in [np.cos, np.sin, np.zeros_like]]
        lon0 = self.EARTH_RADIUS * np.vstack((cth, zth, sth))
        longitudes = []
        lonBase = np.arange(-180, 180, 15)
        for phi in np.radians(lonBase):
            cph, sph = [f(phi) for f in [np.cos, np.sin]]
            lon = np.vstack((lon0[0] * cph - lon0[1] * sph,
                             lon0[1] * cph + lon0[0] * sph,
                             lon0[2]))
            longitudes.append(lon)
        lats = []
        latBase = np.arange(-75, 90, 15)
        for phi in np.radians(latBase):
            cph, sph = [f(phi) for f in [np.cos, np.sin]]
            lat = self.EARTH_RADIUS * np.vstack((cth * cph, sth * cph, zth + sph))
            lats.append(lat)

        # plotting sphere and labels
        for i, longitude in enumerate(longitudes):
            x, y, z = longitude
            axe.plot(x, y, z, '-k', lw=1,
                     color=self.M_GREY)
        for i, lat in enumerate(lats):
            x, y, z = lat
            axe.plot(x, y, z, '-k', lw=1,
                     color=self.M_GREY)

        axe.plot([0, 0],
                 [0, 0],
                 [- self.EARTH_RADIUS * 1.1, self.EARTH_RADIUS * 1.1],
                 lw=3,
                 color=self.M_BLUE)
        axe.text(0, 0, self.EARTH_RADIUS * 1.2, 'N',
                 fontsize=14,
                 color=self.M_BLUE)
        axe.text(0, 0, - self.EARTH_RADIUS * 1.2 - 200, 'S',
                 fontsize=14,
                 color=self.M_BLUE)

        # empty chart if no satellite is chosen
        if observe is None:
            axe.figure.canvas.draw()
            return False

        # drawing satellite
        x, y, z = observe.position.km
        axe.plot(x, y, z, color=self.M_YELLOW)

        self.plotSatPosSphere1, = axe.plot([x[0]], [y[0]], [z[0]],
                                           marker=self.markerSatellite(),
                                           markersize=16,
                                           linewidth=2,
                                           fillstyle='none',
                                           color=self.M_PINK)
        self.makeCubeLimits(axe)
        axe.figure.canvas.draw()
        return True

    def drawSphere2(self, observe=None, subpoint=None):
        """
        draw sphere and put face color als image overlay:

        https://stackoverflow.com/questions/53074908/
        map-an-image-onto-a-sphere-and-plot-3d-trajectories

        but performance problems

        see also:

        https://space.stackexchange.com/questions/25958/
        how-can-i-plot-a-satellites-orbit-in-3d-from-a-tle-using-python-and-skyfield

        :param observe:
        :param subpoint:
        :return: success
        """

        figure = self.satSphereMat2.figure
        figure.clf()
        figure.subplots_adjust(left=-0.1, right=1.1, bottom=-0.3, top=1.2)
        axe = figure.add_subplot(111, projection='3d')

        # switching all visual grids and planes off
        axe.set_facecolor((0, 0, 0, 0))
        axe.tick_params(colors=self.M_GREY,
                        labelsize=12)
        axe.set_axis_off()
        axe.w_xaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))
        axe.w_yaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))
        axe.w_zaxis.set_pane_color((0.0, 0.0, 0.0, 0.0))

        # calculating sphere
        theta = np.linspace(0, 2 * np.pi, 51)
        cth, sth, zth = [f(theta) for f in [np.cos, np.sin, np.zeros_like]]
        lon0 = self.EARTH_RADIUS * np.vstack((cth, zth, sth))
        longitudes = []
        lonBase = np.arange(-180, 180, 15)
        for phi in np.radians(lonBase):
            cph, sph = [f(phi) for f in [np.cos, np.sin]]
            lon = np.vstack((lon0[0] * cph - lon0[1] * sph,
                             lon0[1] * cph + lon0[0] * sph,
                             lon0[2]))
            longitudes.append(lon)
        lats = []
        latBase = np.arange(-75, 90, 15)
        for phi in np.radians(latBase):
            cph, sph = [f(phi) for f in [np.cos, np.sin]]
            lat = self.EARTH_RADIUS * np.vstack((cth * cph, sth * cph, zth + sph))
            lats.append(lat)

        # plotting sphere and labels
        for i, longitude in enumerate(longitudes):
            x, y, z = longitude
            axe.plot(x, y, z, '-k', lw=1,
                     color=self.M_GREY)
        for i, lat in enumerate(lats):
            x, y, z = lat
            axe.plot(x, y, z, '-k', lw=1,
                     color=self.M_GREY)

        axe.plot([0, 0],
                 [0, 0],
                 [- self.EARTH_RADIUS * 1.1, self.EARTH_RADIUS * 1.1],
                 lw=3,
                 color=self.M_BLUE)
        axe.text(0, 0, self.EARTH_RADIUS * 1.2, 'N',
                 fontsize=14,
                 color=self.M_BLUE)
        axe.text(0, 0, - self.EARTH_RADIUS * 1.2 - 200, 'S',
                 fontsize=14,
                 color=self.M_BLUE)

        # plot world
        for key in self.world.keys():
            shape = self.world[key]
            x, y, z = transform.sphericalToCartesian(azimuth=shape['xRad'],
                                                     altitude=shape['yRad'],
                                                     radius=self.EARTH_RADIUS)
            verts = [list(zip(x, y, z))]
            collect = Poly3DCollection(verts, facecolors=self.M_BLUE, alpha=0.5)
            axe.add_collection3d(collect)

        # drawing home position location on earth
        lat = self.app.mount.obsSite.location.latitude.radians
        lon = self.app.mount.obsSite.location.longitude.radians
        x, y, z = transform.sphericalToCartesian(altitude=lat,
                                                 azimuth=lon,
                                                 radius=self.EARTH_RADIUS)
        axe.plot([x], [y], [z],
                 marker='.',
                 markersize=12,
                 color=self.M_RED,
                 )

        # empty chart if no satellite is chosen
        if observe is None:
            axe.figure.canvas.draw()
            return False

        # drawing satellite subpoint path
        lat = subpoint.latitude.radians
        lon = subpoint.longitude.radians
        elev = subpoint.elevation.m / 1000 + self.EARTH_RADIUS
        x, y, z = transform.sphericalToCartesian(azimuth=lon,
                                                 altitude=lat,
                                                 radius=elev)
        axe.plot(x, y, z, color=self.M_YELLOW)

        # draw satellite position
        self.plotSatPosSphere2, = axe.plot([x[0]], [y[0]], [z[0]],
                                           marker=self.markerSatellite(),
                                           markersize=16,
                                           linewidth=2,
                                           fillstyle='none',
                                           color=self.M_PINK)

        # finalizing
        self.makeCubeLimits(axe)
        axe.figure.canvas.draw()

        return True

    def drawEarth(self, subpoint=None):
        """
        drawEarth show a full earth view with the path of the subpoint of the satellite
        drawn on it.

        :param subpoint:
        :return: success
        """

        figure = self.satEarthMat.figure
        figure.clf()
        figure.subplots_adjust(left=0.2, right=0.85, bottom=0.2, top=0.9)
        axe = self.satEarthMat.figure.add_subplot(1, 1, 1, facecolor=None)

        axe.set_facecolor((0, 0, 0, 0))
        axe.set_xlim(-180, 180)
        axe.set_ylim(-90, 90)
        axe.spines['bottom'].set_color(self.M_BLUE)
        axe.spines['top'].set_color(self.M_BLUE)
        axe.spines['left'].set_color(self.M_BLUE)
        axe.spines['right'].set_color(self.M_BLUE)
        axe.grid(True, color=self.M_GREY)
        axe.tick_params(axis='x',
                        colors=self.M_BLUE,
                        labelsize=12)
        axe.set_xticks(np.arange(-180, 181, 45))
        axe.tick_params(axis='y',
                        colors=self.M_BLUE,
                        which='both',
                        labelleft=True,
                        labelright=True,
                        labelsize=12)
        axe.set_xlabel('Longitude in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        axe.set_ylabel('Latitude in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        # plot world
        for key in self.world.keys():
            shape = self.world[key]
            axe.fill(shape['xDeg'], shape['yDeg'], color=self.M_BLUE, alpha=0.2)
            axe.plot(shape['xDeg'], shape['yDeg'], color=self.M_BLUE, lw=1, alpha=0.4)

        # mark the site location in the map
        lat = self.app.mount.obsSite.location.latitude.degrees
        lon = self.app.mount.obsSite.location.longitude.degrees
        axe.plot(lon,
                 lat,
                 marker='.',
                 markersize=10,
                 color=self.M_RED)

        # empty chart if no satellite is chosen
        if subpoint is None:
            axe.figure.canvas.draw()
            return False

        # drawing satellite subpoint path
        lat = subpoint.latitude.degrees
        lon = subpoint.longitude.degrees

        axe.plot(lon,
                 lat,
                 marker='o',
                 markersize=1,
                 linestyle='none',
                 color=self.M_YELLOW)

        # show the actual position
        self.plotSatPosEarth, = axe.plot(lon[0],
                                         lat[0],
                                         marker=self.markerSatellite(),
                                         markersize=16,
                                         linewidth=2,
                                         fillstyle='none',
                                         linestyle='none',
                                         color=self.M_PINK)
        axe.figure.canvas.draw()
        return True

    def staticHorizon(self, axes=None):
        """

        :param axes: matplotlib axes object
        :return:
        """

        if not self.app.data.horizonP:
            return False

        alt, az = zip(*self.app.data.horizonP)

        axes.fill(az, alt, color=self.M_GREEN, zorder=-20, alpha=0.2)
        axes.plot(az, alt, color=self.M_GREEN, zorder=-20, lw=2, alpha=0.4)

        return True

    def drawHorizonView(self, difference=None):
        """
        drawHorizonView shows the horizon and enable the users to explore a satellite
        passing by

        :param difference:
        :return: success
        """

        figure = self.satHorizonMat.figure
        figure.clf()
        figure.subplots_adjust(left=0.2, right=0.85, bottom=0.2, top=0.9)
        axe = self.satHorizonMat.figure.add_subplot(1, 1, 1, facecolor=None)

        # add horizon limit if selected
        self.staticHorizon(axes=axe)

        axe.set_facecolor((0, 0, 0, 0))
        axe.set_xlim(0, 360)
        axe.set_ylim(0, 90)
        axe.spines['bottom'].set_color(self.M_BLUE)
        axe.spines['top'].set_color(self.M_BLUE)
        axe.spines['left'].set_color(self.M_BLUE)
        axe.spines['right'].set_color(self.M_BLUE)
        axe.grid(True, color=self.M_GREY)
        axe.tick_params(axis='x',
                        colors=self.M_BLUE,
                        labelsize=12)
        axeTop = axe.twiny()
        axeTop.set_facecolor((0, 0, 0, 0))
        axeTop.set_xlim(0, 360)
        axeTop.tick_params(axis='x',
                           top=True,
                           colors='#2090C0',
                           labelsize=12)
        axeTop.set_xticks(np.arange(0, 361, 45))
        axeTop.grid(axis='both', visible=False)
        axeTop.set_xticklabels(['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'])
        axeTop.spines['bottom'].set_color('#2090C0')
        axeTop.spines['top'].set_color('#2090C0')
        axeTop.spines['left'].set_color('#2090C0')
        axeTop.spines['right'].set_color('#2090C0')

        axe.set_xticks(np.arange(0, 361, 45))
        axe.tick_params(axis='y',
                        colors=self.M_BLUE,
                        which='both',
                        labelleft=True,
                        labelright=True,
                        labelsize=12)
        axe.set_xlabel('Azimuth in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)
        axe.set_ylabel('Altitude in degrees',
                       color=self.M_BLUE,
                       fontweight='bold',
                       fontsize=12)

        # empty chart if no satellite is chosen
        if difference is None:
            axe.figure.canvas.draw()
            return False

        # orbital calculations
        alt, az, _ = difference.altaz()
        alt = alt.degrees
        az = az.degrees

        # draw path
        axe.plot(az,
                 alt,
                 marker='o',
                 markersize=1,
                 linestyle='none',
                 color=self.M_YELLOW)

        # draw actual position
        self.plotSatPosHorizon, = axe.plot(az[0],
                                           alt[0],
                                           marker=self.markerSatellite(),
                                           markersize=16,
                                           linewidth=2,
                                           fillstyle=None,
                                           linestyle='none',
                                           color=self.M_PINK)

        axe.figure.canvas.draw()
        return True

    def drawSatellite(self):
        """
        drawSatellite draws 3 different views of the actual satellite situation: a sphere
        a horizon view and an earth view.

        :return: True for test purpose
        """

        timescale = self.app.mount.obsSite.ts
        forecast = np.arange(0, self.FORECAST_TIME, 0.005 * self.FORECAST_TIME / 3) / 24
        now = timescale.now()
        timeVector = timescale.tt_jd(now.tt + forecast)

        if self.satellite is not None:
            observe = self.satellite.at(timeVector)
            subpoint = observe.subpoint()
            difference = (self.satellite - self.app.mount.obsSite.location).at(timeVector)
        else:
            observe = subpoint = difference = None

        self.drawSphere1(observe=observe)
        self.drawSphere2(observe=observe, subpoint=subpoint)
        self.drawEarth(subpoint=subpoint)
        self.drawHorizonView(difference=difference)

        return True
Exemplo n.º 30
0
class MyApp(PyQt5.QtWidgets.QApplication):
    """
    MyApp implements a custom notify handler to log errors, when C++ classes and python
    wrapper in PyQt5 environment mismatch. mostly this relates to the situation when a
    C++ object is already deleted, but the python wrapper still exists. so far I know
    that's the only chance to log this issues.

    in addition it writes mouse pressed and key pressed events in debug level to log
    including event and object name to be analyse the input methods.
    """

    logger = logging.getLogger(__name__)
    log = CustomLogger(logger, {})
    last = None

    def handleButtons(self, obj, returnValue):
        """

        :param obj:
        :param returnValue:
        :return:
        """

        if obj.objectName() == 'MainWindowWindow':
            return returnValue

        if obj == self.last:
            return returnValue
        else:
            self.last = obj

        if isinstance(obj, PyQt5.QtWidgets.QTabBar):
            self.log.warning(
                f'Click Tab     : [{obj.tabText(obj.currentIndex())}]')
        elif isinstance(obj, PyQt5.QtWidgets.QComboBox):
            self.log.warning(f'Click DropDown: [{obj.objectName()}]')
        elif isinstance(obj, PyQt5.QtWidgets.QPushButton):
            self.log.warning(f'Click Button  : [{obj.objectName()}]')
        elif isinstance(obj, PyQt5.QtWidgets.QRadioButton):
            self.log.warning(
                f'Click Radio   : [{obj.objectName()}]:{obj.isChecked()}')
        elif isinstance(obj, PyQt5.QtWidgets.QGroupBox):
            self.log.warning(
                f'Click Group   : [{obj.objectName()}]:{obj.isChecked()}')
        elif isinstance(obj, PyQt5.QtWidgets.QCheckBox):
            self.log.warning(
                f'Click Checkbox: [{obj.objectName()}]:{obj.isChecked()}')
        elif isinstance(obj, PyQt5.QtWidgets.QLineEdit):
            self.log.warning(
                f'Click EditLine: [{obj.objectName()}]:{obj.text()}')
        else:
            self.log.warning(f'Click Object  : [{obj.objectName()}]')

        return returnValue

    def notify(self, obj, event):
        """

        :param obj:
        :param event:
        :return:
        """

        try:
            returnValue = PyQt5.QtWidgets.QApplication.notify(self, obj, event)
        except Exception as e:
            self.log.critical(
                '----------------------------------------------------')
            self.log.critical('Event: {0}'.format(event))
            self.log.critical('EventType: {0}'.format(event.type()))
            self.log.critical('Exception error in event loop: {0}'.format(e))
            self.log.critical(
                '----------------------------------------------------')
            returnValue = False

        if not isinstance(event, PyQt5.QtGui.QMouseEvent):
            return returnValue

        if not event.button():
            return returnValue

        returnValue = self.handleButtons(obj, returnValue)

        return returnValue