diff --git a/aircraft.py b/aircraft.py index f9eef9d..1789ee0 100755 --- a/aircraft.py +++ b/aircraft.py @@ -7,171 +7,11 @@ import configuration import lib.recurring_task as recurring_task from lib.simulated_values import SimulatedValue +from logging_object import LoggingObject HEADING_NOT_AVAILABLE = '---' -class StratuxStatus(object): - - def __get_status__(self, key): - if key is None: - return False - - if self.__status_json__ is None: - return False - - if key in self.__status_json__: - try: - return bool(self.__status_json__[key]) - except KeyboardInterrupt: - raise - except SystemExit: - raise - except: - return False - - return False - - def __init__(self, stratux_address, stratux_session, simulation_mode=False): - """ - Builds a list of Capabilities of the stratux. - """ - - if stratux_address is None or simulation_mode: - self.__status_json__ = None - self.cpu_temp = 50.0 - self.satellites_locked = 0 - - else: - url = "http://{0}/getStatus".format(stratux_address) - - try: - self.__status_json__ = stratux_session.get( - url, timeout=2).json() - - except KeyboardInterrupt: - raise - except SystemExit: - raise - except: - self.__status_json__ = {} - - self.cpu_temp = self.__get_status__('CPUTemp') - self.satellites_locked = self.__get_status__( - 'GPS_satellites_locked') - - -class StratuxCapabilities(object): - """ - Get the capabilities of the Stratux, so we know what can be used - in the HUD. - """ - - def __get_capability__(self, key): - if key is None: - return False - - if self.__capabilities_json__ is None: - return False - - if key in self.__capabilities_json__: - try: - return bool(self.__capabilities_json__[key]) - except KeyboardInterrupt: - raise - except SystemExit: - raise - except: - return False - - return False - - def __init__(self, stratux_address, stratux_session, simulation_mode=False): - """ - Builds a list of Capabilities of the stratux. - """ - - if stratux_address is None or simulation_mode: - self.__capabilities_json__ = None - self.traffic_enabled = False - self.gps_enabled = False - self.barometric_enabled = True - self.ahrs_enabled = True - else: - url = "http://{0}/getSettings".format(stratux_address) - - try: - self.__capabilities_json__ = stratux_session.get( - url, timeout=2).json() - - except KeyboardInterrupt: - raise - except SystemExit: - raise - except: - self.__capabilities_json__ = {} - - self.traffic_enabled = self.__get_capability__('UAT_Enabled') - self.gps_enabled = self.__get_capability__('GPS_Enabled') - self.barometric_enabled = self.__get_capability__( - 'BMP_Sensor_Enabled') - self.ahrs_enabled = self.__get_capability__('IMU_Sensor_Enabled') - - # http://192.168.10.1/getSettings - get device settings. Example output: - # { - # "UAT_Enabled": true, - # "ES_Enabled": false, - # "Ping_Enabled": false, - # "GPS_Enabled": true, - # "BMP_Sensor_Enabled": true, - # "IMU_Sensor_Enabled": true, - # "NetworkOutputs": [ - # { - # "Conn": null, - # "Ip": "", - # "Port": 4000, - # "Capability": 5, - # "MessageQueueLen": 0, - # "LastUnreachable": "0001-01-01T00:00:00Z", - # "SleepFlag": false, - # "FFCrippled": false - # } - # ], - # "SerialOutputs": null, - # "DisplayTrafficSource": false, - # "DEBUG": false, - # "ReplayLog": false, - # "AHRSLog": false, - # "IMUMapping": [ - # -1, - # 0 - # ], - # "SensorQuaternion": [ - # 0.0068582877312501, - # 0.0067230280142738, - # 0.7140806859355, - # -0.69999752767998 - # ], - # "C": [ - # -0.019065523239845, - # -0.99225684377575, - # -0.019766228217414 - # ], - # "D": [ - # -2.7707754753258, - # 5.544145023957, - # -1.890621662038 - # ], - # "PPM": 0, - # "OwnshipModeS": "F00000", - # "WatchList": "", - # "DeveloperMode": false, - # "GLimits": "", - # "StaticIps": [ - # ] - # } - - class AhrsData(object): """ Class to hold the AHRS data @@ -184,7 +24,10 @@ def get_onscreen_projection_heading(self): if self.__is_compass_heading_valid__(): return int(self.compass_heading) - return int(self.gps_heading) + if self.gps_online: + return int(self.gps_heading) + + return HEADING_NOT_AVAILABLE def get_onscreen_projection_display_heading(self): try: @@ -195,15 +38,27 @@ def get_onscreen_projection_display_heading(self): return HEADING_NOT_AVAILABLE + def get_onscreen_gps_heading(self): + """ + Returns a safe display version of the GPS heading + """ + return self.gps_heading if self.gps_online else HEADING_NOT_AVAILABLE + def get_heading(self): try: - if self.compass_heading is None or self.compass_heading > 360 or self.compass_heading < 0 or self.compass_heading is '': + if (self.compass_heading is None + or self.compass_heading > 360 + or self.compass_heading < 0 + or self.compass_heading is '') and self.gps_online: return int(self.gps_heading) - return int(self.compass_heading) + if __is_compass_heading_valid__(): + return int(self.compass_heading) except: return HEADING_NOT_AVAILABLE + return HEADING_NOT_AVAILABLE + def __init__(self): self.roll = 0.0 self.pitch = 0.0 @@ -216,6 +71,7 @@ def __init__(self): self.vertical_speed = 0 self.g_load = 1.0 self.utc_time = datetime.datetime.utcnow() + self.gps_online = True class AhrsSimulation(object): @@ -252,10 +108,8 @@ def __init__(self): self.speed_simulator = SimulatedValue(5, 10, 1, 85) self.alt_simulator = SimulatedValue(10, 100, -1, 0, 200) - self.capabilities = StratuxCapabilities(None, None, True) - -class AhrsStratux(object): +class AhrsStratux(LoggingObject): """ Class to pull actual AHRS data from a Stratux (or Stratus) """ @@ -306,42 +160,49 @@ def update(self): configuration.CONFIGURATION.stratux_address()) try: - ahrs_json = self.__stratux_session__.get( - url, timeout=self.__timeout__).json() - self.__last_update__ = datetime.datetime.utcnow( - ) if ahrs_json is not None else self.__last_update__ + ahrs_json = self.__stratux_session__.get(url, + timeout=self.__timeout__).json() + + if ahrs_json is not None: + self.__last_update__ = datetime.datetime.utcnow() except KeyboardInterrupt: raise except SystemExit: raise - except: + except Exception as ex: # If we are spamming the REST too quickly, then we may loose a single update. # Do no consider the service unavailable unless we are # way below the max target framerate. delta_time = datetime.datetime.utcnow() - self.__last_update__ - self.data_source_available = delta_time.total_seconds() < ( - self.__min_update_microseconds__ / 1000000.0) + self.data_source_available = delta_time.total_seconds() < self.__min_update_seconds__ + + self.warn('AHRS.update() ex={}'.format(ex)) return + system_utc_time = str(datetime.datetime.utcnow()) + new_ahrs_data.roll = self.__get_value__(ahrs_json, 'AHRSRoll', 0.0) new_ahrs_data.pitch = self.__get_value__(ahrs_json, 'AHRSPitch', 0.0) new_ahrs_data.compass_heading = self.__get_value__( ahrs_json, 'AHRSGyroHeading', 1080) # anything above 360 indicates "not available" + new_ahrs_data.gps_online = self.__get_value__( + ahrs_json, 'GPSFixQuality', 0) > 0 new_ahrs_data.gps_heading = self.__get_value__( - ahrs_json, 'GPSTrueCourse', 0.0) + ahrs_json, 'GPSTrueCourse', 0.0) if new_ahrs_data.gps_online else HEADING_NOT_AVAILABLE new_ahrs_data.alt = self.__get_value_with_fallback__( - ahrs_json, ['GPSAltitudeMSL', 'BaroPressureAltitude'], None) - new_ahrs_data.position = ( - ahrs_json['GPSLatitude'], ahrs_json['GPSLongitude']) + ahrs_json, ['GPSAltitudeMSL', 'BaroPressureAltitude'], None) if new_ahrs_data.gps_online else HEADING_NOT_AVAILABLE + new_ahrs_data.position = (self.__get_value__(ahrs_json, 'GPSLatitude', None), + self.__get_value__(ahrs_json, 'GPSLongitude', None)) new_ahrs_data.vertical_speed = self.__get_value__( - ahrs_json, 'GPSVerticalSpeed', 0.0) + ahrs_json, 'GPSVerticalSpeed', 0.0) if new_ahrs_data.gps_online else HEADING_NOT_AVAILABLE new_ahrs_data.groundspeed = self.__get_value__( - ahrs_json, 'GPSGroundSpeed', 0.0) + ahrs_json, 'GPSGroundSpeed', 0.0) if new_ahrs_data.gps_online else HEADING_NOT_AVAILABLE new_ahrs_data.g_load = self.__get_value__(ahrs_json, 'AHRSGLoad', 1.0) new_ahrs_data.utc_time = self.__get_value_with_fallback__( - ahrs_json, 'GPSTime', str(datetime.datetime.utcnow())) + ahrs_json, 'GPSTime', system_utc_time) if new_ahrs_data.gps_online else system_utc_time + self.data_source_available = True # except: # self.data_source_available = False @@ -350,45 +211,47 @@ def update(self): # SAMPLE FULL JSON # - # {u'GPSAltitudeMSL': 68.041336, - # u'GPSFixQuality': 1, - # u'AHRSGLoadMin': 0.3307450162084107 - # u'GPSHorizontalAccuracy': 4.2, - # u'GPSLongitude': -122.36627, - # u'GPSGroundSpeed': 16.749273158117294, - # u'GPSLastFixLocalTime': u'0001-01-01T00:06:49.36Z', - # u'AHRSMagHeading': 3276.7, - # u'GPSSatellites': 7, - # u'GPSSatellitesTracked': 12, - # u'BaroPressureAltitude': -149.82413, - # u'GPSPositionSampleRate': 0, - # u'AHRSPitch': -1.6670512276023939, - # u'GPSSatellitesSeen': 12, - # u'GPSLastValidNMEAMessage': u'$PUBX,00,163529.60,4740.16729,N,12221.97653,W,1.939,G3,2.1,3.2,31.017,179.98,0.198,,1.93,2.43,1.89,7,0,0*4D', - # u'AHRSSlipSkid': -25.030695817203796, - # u'GPSLastGPSTimeStratuxTime': u'0001-01-01T00:06:48.76Z', - # u'GPSLastFixSinceMidnightUTC': 59729.6, - # u'GPSLastValidNMEAMessageTime': u'0001-01-01T00:06:49.36Z', - # u'GPSNACp': 10, - # u'AHRSLastAttitudeTime': u'0001-01-01T00:06:49.4Z', - # u'GPSTurnRate': 0, - # u'AHRSTurnRate': -0.2607137769860283, - # u'GPSLastGroundTrackTime': u'0001-01-01T00:06:49.36Z', - # u'BaroVerticalSpeed': -11.46994, - # u'GPSTrueCourse': 179.98, - # u'BaroLastMeasurementTime': u'0001-01-01T00:06:49.4Z', - # u'GPSVerticalAccuracy': 6.4, - # u'AHRSGLoad': 0.8879934248943415, - # u'BaroTemperature': 30.09, - # u'AHRSGyroHeading': 184.67916154869323, - # u'AHRSRoll': 26.382463342051672, - # u'GPSGeoidSep': -61.67979, - # u'AHRSGLoadMax': 1.0895587458493998, - # u'GPSTime': u'2018-02-26T16:35:29Z', - # u'GPSVerticalSpeed': -0.6496063, - # u'GPSHeightAboveEllipsoid': 6.361549, - # u'GPSLatitude': 47.669456, - # u'AHRSStatus': 7} + # { + # "GPSLastFixSinceMidnightUTC": 26705.5, + # "GPSLatitude": 47.69124, + # "GPSLongitude": -122.36745, + # "GPSFixQuality": 2, + # "GPSHeightAboveEllipsoid": 239.07481, + # "GPSGeoidSep": -61.35171, + # "GPSSatellites": 11, + # "GPSSatellitesTracked": 18, + # "GPSSatellitesSeen": 14, + # "GPSHorizontalAccuracy": 2.2, + # "GPSNACp": 11, + # "GPSAltitudeMSL": 300.4265, + # "GPSVerticalAccuracy": 4.4, + # "GPSVerticalSpeed": -0.39041996, + # "GPSLastFixLocalTime": "0001-01-01T00:59:50.37Z", + # "GPSTrueCourse": 0, + # "GPSTurnRate": 0, + # "GPSGroundSpeed": 0.09990055628746748, + # "GPSLastGroundTrackTime": "0001-01-01T00:59:50.37Z", + # "GPSTime": "2019-06-03T07:25:04.6Z", + # "GPSLastGPSTimeStratuxTime": "0001-01-01T00:59:49.47Z", + # "GPSLastValidNMEAMessageTime": "0001-01-01T00:59:50.37Z", + # "GPSLastValidNMEAMessage": "$PUBX,00,072505.50,4741.47430,N,12222.04701,W,72.870,D3,1.1,2.2,0.185,289.03,0.119,,1.07,1.64,1.20,11,0,0*79", + # "GPSPositionSampleRate": 0, + # "BaroTemperature": 36.12, + # "BaroPressureAltitude": 243.29552, + # "BaroVerticalSpeed": 1.0008061, + # "BaroLastMeasurementTime": "0001-01-01T00:59:50.36Z", + # "AHRSPitch": -0.04836615637379224, + # "AHRSRoll": -0.36678574817765497, + # "AHRSGyroHeading": 3276.7, + # "AHRSMagHeading": 3276.7, + # "AHRSSlipSkid": -0.05914792289016943, + # "AHRSTurnRate": 3276.7, + # "AHRSGLoad": 0.9988800063331206, + # "AHRSGLoadMin": -0.0006306474610048851, + # "AHRSGLoadMax": 1.0107446345882283, + # "AHRSLastAttitudeTime": "0001-01-01T00:59:50.43Z", + # "AHRSStatus": 7 + # } def __set_ahrs_data__(self, new_ahrs_data): """ @@ -398,40 +261,24 @@ def __set_ahrs_data__(self, new_ahrs_data): self.ahrs_data = new_ahrs_data self.__lock__.release() - def __update_capabilities__(self): - """ - Check occasionally to see if the settings - for the Stratux have been changed that would - affect what we should show and what is actually - available. - """ - self.__lock__.acquire() - try: - self.capabilities = StratuxCapabilities( - configuration.CONFIGURATION.stratux_address(), self.__stratux_session__) - self.stratux_status = StratuxStatus( - configuration.CONFIGURATION.stratux_address(), self.__stratux_session__) - finally: - self.__lock__.release() + def __init__(self, logger): + super(AhrsStratux, self).__init__(logger) - def __init__(self): - self.__min_update_microseconds__ = int( - 1000000.0 / (configuration.MAX_FRAMERATE / 10.0)) - self.__timeout__ = 1.0 / (configuration.MAX_FRAMERATE / 8.0) + # If an update to the AHRS takes longer than this, + # then the AHRS should be considered not available. + self.__min_update_seconds__ = 0.3 + # Make the timeout a reasonable time. + self.__timeout__ = configuration.AHRS_TIMEOUT self.__stratux_session__ = requests.Session() self.ahrs_data = AhrsData() self.data_source_available = False - self.capabilities = StratuxCapabilities( - configuration.CONFIGURATION.stratux_address(), self.__stratux_session__) - recurring_task.RecurringTask( - 'UpdateCapabilities', 15, self.__update_capabilities__) self.__lock__ = threading.Lock() self.__last_update__ = datetime.datetime.utcnow() -class Aircraft(object): +class Aircraft(LoggingObject): def update_orientation_in_background(self): print("starting") while True: @@ -439,19 +286,21 @@ def update_orientation_in_background(self): self.__update_orientation__() except KeyboardInterrupt: raise - except: - print("error") + except Exception as ex: + self.warn("update_orientation_in_background ex={}".format(ex)) + + def __init__(self, logger=None, force_simulation=False): + super(Aircraft, self).__init__(logger) - def __init__(self, force_simulation=False): self.ahrs_source = None if force_simulation or configuration.CONFIGURATION.data_source() == configuration.DataSourceNames.SIMULATION: self.ahrs_source = AhrsSimulation() elif configuration.CONFIGURATION.data_source() == configuration.DataSourceNames.STRATUX: - self.ahrs_source = AhrsStratux() + self.ahrs_source = AhrsStratux(logger) recurring_task.RecurringTask('UpdateAhrs', - 1.0 / (configuration.MAX_FRAMERATE * 2.0), + 1.0 / configuration.TARGET_AHRS_FRAMERATE, self.__update_orientation__) def is_ahrs_available(self): diff --git a/aithre.py b/aithre.py new file mode 100644 index 0000000..38f5fe6 --- /dev/null +++ b/aithre.py @@ -0,0 +1,231 @@ +import sys +import time +import datetime +from configuration import CONFIGURATION +import lib.local_debug as local_debug + +if local_debug.IS_LINUX: + from bluepy.btle import UUID, Peripheral, Scanner, DefaultDelegate +else: + from lib.simulated_values import SimulatedValue + aithre_co_simulator = SimulatedValue(1, 50, 1, -25, 25) + aithre_bat_simulator = SimulatedValue(1, 50, -1, 35, 50) + +# The Aithre is always expected to have a public address +AITHRE_ADDR_TYPE = "public" + +# Service UUID for the carbon monoxide reading. +# Will be a single character whose ASCII +# value is the parts per milloion 0 - 255 inclusive +CO_OFFSET = "BCD466FE07034D85A021AE8B771E4922" + +# A single character wholes ASCII value is +# the percentage of the battert reminaing. +# The value will be 0 to 100 inclusive. +BAT_OFFSET = "24509DDEFCD711E88EB2F2801F1B9FD1" + +CO_SAFE = 10 +CO_WARNING = 49 + +BATTERY_SAFE = 75 +BATTERY_WARNING = 25 + + +def get_service_value(addr, addr_type, offset): + """ + Gets the value from a Blue Tooth Low Energy device. + Arguments: + addr {string} -- The address to get the value from + add_type {string} -- The type of address we are using. + offset {string} -- The offset from the device's address to get the value from + Returns: {int} -- The result of the fetch + """ + if not CONFIGURATION.aithre_enabled: + return None + + # Generate fake values for debugging + # and for the development of the visuals. + if not local_debug.IS_LINUX: + if offset in CO_OFFSET: + return int(aithre_co_simulator.get_value()) + else: + return int(aithre_bat_simulator.get_value()) + + try: + p = Peripheral(addr, addr_type) # bluepy.btle.ADDR_TYPE_PUBLIC) + ch_all = p.getCharacteristics(uuid=offset) + + if ch_all[0].supportsRead(): + res = ch_all[0].read() + + p.disconnect() + + return ord(res) + except Exception as ex: + print(" ex in get_name={}".format(ex)) + + return None + + +def get_aithre(mac_adr): + """ + Gets the current Aithre readings given a MAC for the Aithre + Arguments: + mac_adr {string} -- The MAC address of the Aithre to fetch from. + Returns: {(int, int)} -- The co and battery percentage of the Aithre + """ + if not CONFIGURATION.aithre_enabled: + return None, None + + co = get_service_value(mac_adr, AITHRE_ADDR_TYPE, CO_OFFSET) + bat = get_service_value(mac_adr, AITHRE_ADDR_TYPE, BAT_OFFSET) + + return co, bat + + +def get_aithre_mac(): + """ + Attempts to find an Aithre MAC using Blue Tooth low energy. + Returns: {string} None if a device was not found, otherwise the MAC of the Aithre + """ + if not CONFIGURATION.aithre_enabled: + return None + + try: + if not local_debug.IS_LINUX: + return None + + scanner = Scanner() + devices = scanner.scan(2) + for dev in devices: + print(" {} {} {}".format(dev.addr, dev.addrType, dev.rssi)) + + for (adtype, desc, value) in dev.getScanData(): + try: + if "AITH" in value: + return dev.addr + except Exception as ex: + print("DevScan loop - ex={}".format(ex)) + + except Exception as ex: + print("Outter loop ex={}".format(ex)) + + return None + + +CO_SCAN_PERIOD = 15 + +if local_debug.IS_LINUX: + CO_SCAN_PERIOD = 1.0 + +OFFLINE = "OFFLINE" + + +class Aithre(object): + def log(self, text): + """ + Logs the given text if a logger is available. + + Arguments: + text {string} -- The text to log + """ + + if self.__logger__ is not None: + self.__logger__.log_info_message(text) + else: + print("INFO:{}".format(text)) + + def warn(self, text): + """ + Logs the given text if a logger is available AS A WARNING. + + Arguments: + text {string} -- The text to log + """ + + if self.__logger__ is not None: + self.__logger__.log_warning_message(text) + else: + print("WARN:{}".format(text)) + + def __init__(self, logger = None): + self.__logger__ = logger + + self.warn("Initializing new Aithre object") + + self._mac_ = None + self._levels_ = None + + self._update_mac_() + + def is_connected(self): + return (self._mac_ is not None and self._levels_ is not None) or not local_debug.IS_LINUX + + def update(self): + self._update_levels() + + def _update_mac_(self): + if not CONFIGURATION.aithre_enabled: + return + + try: + self._mac_ = get_aithre_mac() + except Exception as e: + self._mac_ = None + self.warn("Got EX={} during MAC update.".format(e)) + + def _update_levels(self): + if not CONFIGURATION.aithre_enabled: + return + + if self._mac_ is None: + self.log("Aithre MAC is none while attempting to update levels.") + if not local_debug.IS_LINUX: + self.log("... and this is not a Linux machine, so attempting to simulate.") + aithre_co_simulator.simulate() + aithre_bat_simulator.simulate() + else: + self.warn("Aithre MAC is none, attempting to connect.") + self._update_mac_() + + try: + self.log("Attempting update") + self._levels_ = get_aithre(self._mac_) + except Exception as ex: + # In case the read fails, we will want to + # attempt to find the MAC of the Aithre again. + + self._mac_ = None + self.warn("Exception while attempting to update the cached levels.update() E={}".format(ex)) + + def get_battery(self): + if not CONFIGURATION.aithre_enabled: + return None + + if self._levels_ is not None: + return self._levels_[1] + + return OFFLINE + + def get_co_level(self): + if not CONFIGURATION.aithre_enabled: + return None + + if self._levels_ is not None: + return self._levels_[0] + + return OFFLINE + + +# Global singleton for all to +# get to the Aithre +try: + sensor = Aithre() +except: + sensor = None + +if __name__ == '__main__': + while True: + sensor.update() + print("CO:{} BAT{}".format(sensor.get_co_level(), sensor.get_battery())) + time.sleep(CO_SCAN_PERIOD) diff --git a/config.json b/config.json index 06d6af5..97324f1 100755 --- a/config.json +++ b/config.json @@ -1,11 +1,11 @@ { + "aithre": true, "data_source": "stratux", "declination": 0.0, "degrees_of_pitch": 90, "distance_units": "statute", "flip_horizontal": false, "flip_vertical": false, - "ownship": "N701GV", "pitch_degrees_scaler": 4.0, "stratux_address": "192.168.10.1", "traffic_report_removal_minutes": 1.0 diff --git a/configuration.py b/configuration.py index 0c72e15..40d2134 100755 --- a/configuration.py +++ b/configuration.py @@ -1,31 +1,48 @@ import json import os +from os.path import expanduser import units +import requests +import lib.recurring_task as recurring_task +from receiver_capabilities import StratuxCapabilities +from receiver_status import StratuxStatus EARTH_RADIUS_NAUTICAL_MILES = 3440 EARTH_RADIUS_STATUTE_MILES = 3956 EARTH_RADIUS_KILOMETERS_MILES = 6371 MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT = 2 MAX_FRAMERATE = 60 - -VERSION = "1.4.0" - +TARGET_AHRS_FRAMERATE = 60 +AHRS_TIMEOUT = 1.0 + +VERSION = "1.5.0" + +######################## +# Default Config Files # +######################## +# +# Base, default values. +# There are two config files. One is the +# default that everything falls back to +# The other is the user saved and modified +# file that is merged in __config_file__ = './config.json' -__heading_bugs_file__ = './heading_bugs.json' __view_elements_file__ = './elements.json' __views_file__ = './views.json' -__working_dir__ = os.path.dirname(os.path.abspath(__file__)) -def get_config_file_location(): - """ - Returns the location of the configuration file. - - Returns: - string -- The path to the config file - """ +##################### +# User Config Files # +##################### +# +# These are the user modified files +# that are merged in with the system +# defaults, overriding what is set. +__user_views_file__ = '{}/hud_views.json'.format(expanduser('~')) +__user_config_file__ = '{}/hud_config.json'.format(expanduser('~')) +__heading_bugs_file__ = '{}/hud_heading_bugs.json'.format(expanduser('~')) - return __config_file__ +__working_dir__ = os.path.dirname(os.path.abspath(__file__)) def get_absolute_file_path(relative_path): @@ -42,8 +59,8 @@ def get_absolute_file_path(relative_path): return os.path.join(__working_dir__, os.path.normpath(relative_path)) +# System Default Config Files DEFAULT_CONFIG_FILE = get_absolute_file_path(__config_file__) -HEADING_BUGS_FILE = get_absolute_file_path(__heading_bugs_file__) VIEW_ELEMENTS_FILE = get_absolute_file_path(__view_elements_file__) VIEWS_FILE = get_absolute_file_path(__views_file__) @@ -54,6 +71,13 @@ class DataSourceNames(object): class Configuration(object): + ############################### + # Hardcoded Config Fall Backs # + ############################### + # + # These are here in case the user does something + # bad to the default config files. + # DEFAULT_NETWORK_IP = "192.168.10.1" STRATUX_ADDRESS_KEY = "stratux_address" DATA_SOURCE_KEY = "data_source" @@ -65,38 +89,65 @@ class Configuration(object): DECLINATION_KEY = "declination" DEGREES_OF_PITCH_KEY = 'degrees_of_pitch' PITCH_DEGREES_DISPLAY_SCALER_KEY = 'pitch_degrees_scaler' + AITHRE_KEY = 'aithre' DEFAULT_DEGREES_OF_PITCH = 90 DEFAULT_PITCH_DEGREES_DISPLAY_SCALER = 2.0 def get_elements_list(self): - with open(VIEW_ELEMENTS_FILE) as json_config_file: - json_config_text = json_config_file.read() - json_config = json.loads(json_config_text) + """ + Returns the list of elements available for the views. + """ - return json_config + return self.__load_config_from_json_file__(VIEW_ELEMENTS_FILE) - return {} + def __load_views_from_file__(self, file_name): + views_key = 'views' + + try: + full_views_contents = self.__load_config_from_json_file__( + file_name) + + if full_views_content is not None and views_key in full_views_content: + return full_views_contents[views_key] + except: + pass + + return None def get_views_list(self): """ - Returns a list of views that can be used by the HUD + Loads the view configuration file. + First looks for a user configuration file. + If there is one, and the file is valid, then + returns those contents. + + If there is an issue with the user file, + then returns the system level default. Returns: - array -- Array of dictionary. Each element contains the name of the view and a list of elements it is made from. + array -- Array of dictionary. Each element contains the name of the view and a list of elements it is made from. """ try: - with open(VIEWS_FILE) as json_config_file: - json_config_text = json_config_file.read() - json_config = json.loads(json_config_text) + views = self.__load_views_from_file__(__user_views_file__) - return json_config['views'] + if views is not None and any(views): + return views + + return self.__load_views_from_file__(VIEWS_FILE) except: return [] def write_views_list(self, view_config): - with open(VIEWS_FILE, 'w') as configfile: - configfile.write(view_config) + """ + Writes the view configuration to the user's version of the file. + """ + + try: + with open(__user_views_file__, 'w') as configfile: + configfile.write(view_config) + except: + print("ERROR trying to write user views file.") def get_json_from_text(self, text): """ @@ -118,25 +169,29 @@ def get_json_from_config(self): Configuration.DATA_SOURCE_KEY: self.data_source(), Configuration.FLIP_HORIZONTAL_KEY: self.flip_horizontal, Configuration.FLIP_VERTICAL_KEY: self.flip_vertical, - Configuration.OWNSHIP_KEY: self.ownship, Configuration.MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT_KEY: self.max_minutes_before_removal, Configuration.DISTANCE_UNITS_KEY: self.get_units(), Configuration.DECLINATION_KEY: self.get_declination(), Configuration.DEGREES_OF_PITCH_KEY: self.get_degrees_of_pitch(), - Configuration.PITCH_DEGREES_DISPLAY_SCALER_KEY: self.get_pitch_degrees_display_scaler() + Configuration.PITCH_DEGREES_DISPLAY_SCALER_KEY: self.get_pitch_degrees_display_scaler(), + Configuration.AITHRE_KEY: self.aithre_enabled } return json.dumps(config_dictionary, indent=4, sort_keys=True) def write_config(self): """ - Writes the config file down to file. + Writes the config file to the user's file. + """ - config_to_write = self.get_json_from_config() + try: + config_to_write = self.get_json_from_config() - with open(get_config_file_location(), 'w') as configfile: - configfile.write(config_to_write) + with open(__user_config_file__, 'w') as configfile: + configfile.write(config_to_write) + except: + print("ERROR trying to write user config file.") def set_from_json(self, json_config): """ @@ -153,6 +208,11 @@ def set_from_json(self, json_config): if key in json_config: self.__configuration__[key] = json_config[key] + if Configuration.AITHRE_KEY in json_config: + self.aithre_enabled = bool(json_config[Configuration.AITHRE_KEY]) + self.__configuration__[Configuration.AITHRE_KEY] = \ + self.aithre_enabled + if Configuration.FLIP_HORIZONTAL_KEY in json_config: self.flip_horizontal = \ bool(json_config[Configuration.FLIP_HORIZONTAL_KEY]) @@ -165,10 +225,6 @@ def set_from_json(self, json_config): self.__configuration__[Configuration.FLIP_VERTICAL_KEY] = \ self.flip_vertical - if Configuration.OWNSHIP_KEY in json_config: - self.ownship = json_config[Configuration.OWNSHIP_KEY] - self.__configuration__[Configuration.OWNSHIP_KEY] = self.ownship - if Configuration.MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT_KEY in json_config: self.max_minutes_before_removal = float( json_config[Configuration.MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT_KEY]) @@ -270,31 +326,73 @@ def update_configuration(self, json_config): json_config_file {dictionary} -- JSON provided config decoded into a dictionary. """ + if json_config is None: + return + self.__configuration__.update(json_config) self.set_from_json(self.__configuration__) self.write_config() - def __load_configuration__(self, json_config_file): + def __load_config_from_json_file__(self, json_config_file): + """ + Loads the complete configuration into the system. + Uses the default values as a base, then puts the + user's configuration overtop. + """ + try: + with open(json_config_file) as json_config_file: + json_config_text = json_config_file.read() + json_config = json.loads(json_config_text) + + return json_config + except: + return {} + + def __load_configuration__(self, default_config_file, user_config_file): """ Loads the configuration. """ - with open(json_config_file) as json_config_file: - json_config_text = json_config_file.read() - json_config = json.loads(json_config_text) - return json_config + config = self.__load_config_from_json_file__(default_config_file) + user_config = self.__load_config_from_json_file__(user_config_file) + + if user_config is not None: + config.update(user_config) + + return config + + def __update_capabilities__(self): + """ + Check occasionally to see if the settings + for the Stratux have been changed that would + affect what we should show and what is actually + available. + """ + self.capabilities = StratuxCapabilities( + self.stratux_address(), self.__stratux_session__, None) + self.stratux_status = StratuxStatus( + self.stratux_address(), self.__stratux_session__, None) - def __init__(self, json_config_file): + def __init__(self, default_config_file, user_config_file): self.degrees_of_pitch = Configuration.DEFAULT_DEGREES_OF_PITCH self.pitch_degrees_display_scaler = Configuration.DEFAULT_PITCH_DEGREES_DISPLAY_SCALER - self.__configuration__ = self.__load_configuration__(json_config_file) - self.ownship = self.__get_config_value__(Configuration.OWNSHIP_KEY, '') + self.__configuration__ = self.__load_configuration__( + default_config_file, user_config_file) self.max_minutes_before_removal = self.__get_config_value__( Configuration.MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT_KEY, MAX_MINUTES_BEFORE_REMOVING_TRAFFIC_REPORT) self.log_filename = "stratux_hud.log" self.flip_horizontal = False self.flip_vertical = False self.declination = 0.0 + self.aithre_enabled = False + self.__stratux_session__ = requests.Session() + + self.stratux_status = StratuxStatus( + self.stratux_address(), self.__stratux_session__, None) + self.capabilities = StratuxCapabilities( + self.stratux_address(), self.__stratux_session__, None) + recurring_task.RecurringTask( + 'UpdateCapabilities', 15, self.__update_capabilities__) self.set_from_json(self.__configuration__) @@ -326,8 +424,14 @@ def __init__(self, json_config_file): except: pass + try: + self.aithre_enabled = \ + bool(self.__configuration__[Configuration.AITHRE_KEY]) + except: + pass + -CONFIGURATION = Configuration(DEFAULT_CONFIG_FILE) +CONFIGURATION = Configuration(DEFAULT_CONFIG_FILE, __user_config_file__) if __name__ == '__main__': from_config = CONFIGURATION.get_json_from_config() diff --git a/elements.json b/elements.json index 451cd98..7379cfd 100644 --- a/elements.json +++ b/elements.json @@ -1,4 +1,8 @@ { + "Aithre": { + "class": "system_info.Aithre", + "detail_font": true + }, "Compass": { "class": "compass_and_heading_bottom_element.CompassAndHeadingBottomElement", "detail_font": true diff --git a/heads_up_display.py b/heads_up_display.py index cd74e55..5360da2 100755 --- a/heads_up_display.py +++ b/heads_up_display.py @@ -23,6 +23,7 @@ import targets import traffic import restful_host +import aithre from views import (adsb_on_screen_reticles, adsb_target_bugs, adsb_target_bugs_only, adsb_traffic_listing, ahrs_not_available, altitude, artificial_horizon, compass_and_heading_bottom_element, @@ -57,7 +58,7 @@ def __level_ahrs__(self): requests.Session().post(url, timeout=2) except: pass - + def __reset_websocket__(self): """ Resets the websocket to essentially reset the receiver unit. @@ -67,7 +68,6 @@ def __reset_websocket__(self): except: pass - def __shutdown_stratux__(self): """ Sends the command to the Stratux to shutdown. @@ -86,7 +86,8 @@ def run(self): Runs the update/render logic loop. """ - self.log('Initialized screen size to {}x{}'.format(self.__width__, self.__height__)) + self.log('Initialized screen size to {}x{}'.format( + self.__width__, self.__height__)) # Make sure that the disclaimer is visible for long enough. sleep(5) @@ -150,7 +151,7 @@ def tick(self, clock): bool -- True if the code should run for another tick. """ - current_fps = 0 # initialize up front avoids exception + current_fps = 0 # initialize up front avoids exception try: self.frame_setup.start() @@ -206,7 +207,7 @@ def tick(self, clock): self.__fps__.to_string())) self.log('TRAFFIC, {0}, MessagesReceived, {1}, {1}, {1}'.format(now, - traffic.Traffic.TRAFFIC_REPORTS_RECEIVED)) + traffic.Traffic.TRAFFIC_REPORTS_RECEIVED)) traffic.Traffic.TRAFFIC_REPORTS_RECEIVED = 0 self.log("-----------------------------------") @@ -226,9 +227,9 @@ def tick(self, clock): surface, CONFIGURATION.flip_horizontal, CONFIGURATION.flip_vertical) surface.blit(flipped, [0, 0]) pygame.display.update() - clock.tick(MAX_FRAMERATE) self.__fps__.push(current_fps) self.frame_cleanup.stop() + clock.tick(MAX_FRAMERATE) return True @@ -331,7 +332,7 @@ def __build_ahrs_hud_element(self, hud_element_class, use_detail_font=False): def __load_view_elements(self): """ - Loads the list of available view elements from the configuration + Loads the list of available view elements from thee ifconfiguration file. Returns it as a map of the element name (Human/kind) to the Python object that instantiates it, and if it uses the "detail" (aka Large) font or not. @@ -366,6 +367,8 @@ def __load_views(self, view_elements): """ hud_views = [] + existing_elements = {} + elements_requested = 0 with open(VIEWS_FILE) as json_config_file: json_config_text = json_config_file.read() @@ -378,9 +381,21 @@ def __load_views(self, view_elements): new_view_elements = [] for element_name in element_names: + elements_requested += 1 element_config = view_elements[element_name] - new_view_elements.append(self.__build_ahrs_hud_element( - element_config[0], element_config[1])) + element_hash_name = "{}{}".format( + element_config[0], element_config[1]) + + # Instantiating multiple elements of the same type/font + # REALLY chews up memory.. and there is no + # good reason to use new instances anyway. + if element_hash_name not in existing_elements: + new_element = self.__build_ahrs_hud_element(element_config[0], element_config[1]) + existing_elements[element_hash_name] = new_element + + + new_view_elements.append( + existing_elements[element_hash_name]) is_ahrs_view = self.__is_ahrs_view__(new_view_elements) hud_views.append( @@ -389,6 +404,8 @@ def __load_views(self, view_elements): self.log( "While attempting to load view={}, EX:{}".format(view, ex)) + self.log("While loading, {} elements were requested, with {} unique being created.".format(elements_requested, len(existing_elements.keys()))) + return hud_views def __build_hud_views(self): @@ -410,6 +427,30 @@ def __purge_old_reports__(self): def __update_traffic_reports__(self): hud_elements.HudDataCache.update_traffic_reports() + def __update_aithre__(self): + if not CONFIGURATION.aithre_enabled: + return + + if aithre.sensor is not None: + try: + aithre.sensor.update() + self.log("Aithre updated") + if aithre.sensor.is_connected(): + co_level = aithre.sensor.get_co_level() + bat_level = aithre.sensor.get_battery() + + self.log("CO:{}ppm, BAT:{}%".format(co_level, bat_level)) + else: + self.log("Aithre is enabled, but not connected.") + except: + self.warn("Error attempting to update Aithre sensor values") + elif CONFIGURATION.aithre_enabled: + try: + aithre.sensor = aithre.Aithre(self.__logger__) + self.log("Aithre created") + except: + self.warn("Error attempting to connect to Aithre") + def __init__(self, logger): """ Initialize and create a new HUD. @@ -442,14 +483,11 @@ def __init__(self, logger): self.__backpage_framebuffer__, screen_size = display.display_init() # args.debug) self.__width__, self.__height__ = screen_size - pygame.mouse.set_visible(False) pygame.font.init() self.__should_render_perf__ = False - font_name = "consolas,monaco,courier,arial,helvetica" - font_size_std = int(self.__height__ / 10.0) font_size_detail = int(self.__height__ / 12.0) font_size_loading = int(self.__height__ / 4.0) @@ -458,11 +496,11 @@ def __init__(self, logger): get_absolute_file_path("./assets/fonts/LiberationMono-Bold.ttf"), font_size_std) self.__detail_font__ = pygame.font.Font( get_absolute_file_path("./assets/fonts/LiberationMono-Bold.ttf"), font_size_detail) - self.__loading_font__ = pygame.font.SysFont( - font_name, font_size_loading, True, False) + self.__loading_font__ = pygame.font.Font( + get_absolute_file_path("./assets/fonts/LiberationMono-Regular.ttf"), font_size_loading) self.__show_boot_screen__() - self.__aircraft__ = Aircraft() + self.__aircraft__ = Aircraft(self.__logger__) self.__pixels_per_degree_y__ = int((self.__height__ / CONFIGURATION.get_degrees_of_pitch()) * CONFIGURATION.get_pitch_degrees_display_scaler()) @@ -486,6 +524,8 @@ def __init__(self, logger): self.__purge_old_reports__, start_immediate=False) RecurringTask("update_traffic", 0.1, self.__update_traffic_reports__, start_immediate=True) + RecurringTask("update_aithre", 5.0, + self.__update_aithre__, start_immediate=True) def __show_boot_screen__(self): """ @@ -495,9 +535,9 @@ def __show_boot_screen__(self): disclaimer_text = ['Not intended as', 'a primary collision evasion', 'or flight instrument system.', - 'For advisiory only.'] + 'For advisory only.'] - texture = self.__loading_font__.render("BOOTING", True, display.RED) + texture = self.__loading_font__.render("LOADING", True, display.RED) text_width, text_height = texture.get_size() surface = pygame.display.get_surface() @@ -561,7 +601,7 @@ def __handle_key_event__(self, event): if event.key in [pygame.K_ESCAPE]: utilities.shutdown(0) - if not local_debug.is_debug(): + if local_debug.IS_PI: self.__shutdown_stratux__() return False @@ -590,7 +630,7 @@ def __handle_key_event__(self, event): if event.key in [pygame.K_EQUALS, pygame.K_KP_EQUALS]: self.__should_render_perf__ = not self.__should_render_perf__ - + if event.key in [pygame.K_KP0, pygame.K_0, pygame.K_INSERT]: self.__reset_websocket__() diff --git a/hud_elements.py b/hud_elements.py index 3e706b8..b79956b 100644 --- a/hud_elements.py +++ b/hud_elements.py @@ -80,8 +80,11 @@ class HudDataCache(object): @staticmethod def update_traffic_reports(): HudDataCache.__LOCK__.acquire() - HudDataCache.RELIABLE_TRAFFIC = traffic.AdsbTrafficClient.TRAFFIC_MANAGER.get_traffic_with_position() - HudDataCache.__LOCK__.release() + + try: + HudDataCache.RELIABLE_TRAFFIC = traffic.AdsbTrafficClient.TRAFFIC_MANAGER.get_traffic_with_position() + finally: + HudDataCache.__LOCK__.release() @staticmethod def get_reliable_traffic(): @@ -91,9 +94,12 @@ def get_reliable_traffic(): Returns: list -- A list of the reliable traffic. """ + traffic_clone = None HudDataCache.__LOCK__.acquire() - traffic_clone = HudDataCache.RELIABLE_TRAFFIC[:] - HudDataCache.__LOCK__.release() + try: + traffic_clone = HudDataCache.RELIABLE_TRAFFIC[:] + finally: + HudDataCache.__LOCK__.release() return traffic_clone diff --git a/kill_huds.sh b/kill_huds.sh new file mode 100755 index 0000000..99e400b --- /dev/null +++ b/kill_huds.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# +# Kills any StratuxHud processes hanging arround. +# Useful for making sure the REST ports are clear +# and nothing is left over. +sudo kill -9 $(sudo ps -ef | grep -i "hud" | grep -v "grep" | awk '{print $2}' | tac) diff --git a/lib/display.py b/lib/display.py index a07b827..c09fa1b 100644 --- a/lib/display.py +++ b/lib/display.py @@ -31,10 +31,10 @@ def display_init(): size = DEFAULT_SCREEN_SIZE disp_no = os.getenv('DISPLAY') if disp_no: - # if False: - # print "I'm running under X display = {0}".format(disp_no) - size = 320, 240 - screen = pygame.display.set_mode(size) + screen_mode = (pygame.FULLSCREEN if local_debug.IS_PI else pygame.RESIZABLE) \ + | pygame.HWACCEL + print("Running under X{}, flags={}".format(disp_no, screen_mode)) + screen = pygame.display.set_mode(size, screen_mode) else: # List of drivers: # https://wiki.libsdl.org/FAQUsingSDL @@ -65,11 +65,11 @@ def display_init(): # TODO - Use "convert" on text without ALPHA... # https://en.wikipedia.org/wiki/PyPy - if local_debug.is_debug(): - screen_mode |= pygame.RESIZABLE - else: + if local_debug.IS_PI: screen_mode |= pygame.FULLSCREEN size = pygame.display.Info().current_w, pygame.display.Info().current_h + else: + screen_mode |= pygame.RESIZABLE screen = pygame.display.set_mode(size, screen_mode) diff --git a/lib/local_debug.py b/lib/local_debug.py index ac1d0f2..925fad7 100755 --- a/lib/local_debug.py +++ b/lib/local_debug.py @@ -4,13 +4,16 @@ debugging on a Mac or Windows host. """ -from sys import platform - -from sys import platform, version_info +import platform +from sys import platform as os_platform +from sys import version_info REQUIRED_PYTHON_VERSION = 2.7 MAXIMUM_PYTHON_VERSION = 2.7 +IS_LINUX = 'linux' in os_platform +DETECTED_CPU = platform.machine() +IS_PI = "arm" in DETECTED_CPU def validate_python_version(): """ @@ -32,12 +35,11 @@ def validate_python_version(): print('Python version {} is newer than the maximum allowed version of {}'.format( python_version, MAXIMUM_PYTHON_VERSION)) - def is_debug(): """ returns True if this should be run as a local debug (Mac or Windows). """ - return platform in ["win32", "darwin"] + return os_platform in ["win32", "darwin"] validate_python_version() diff --git a/lib/recurring_task.py b/lib/recurring_task.py index 21c74f2..c63bc02 100755 --- a/lib/recurring_task.py +++ b/lib/recurring_task.py @@ -36,12 +36,14 @@ def is_running(self): """ Returns True if the task is running. """ - + result = False self.__lock__.acquire() - result = self.__is_alive__ \ - and self.__task_callback__ is not None \ - and self.__is_running__ - self.__lock__.release() + try: + result = self.__is_alive__ \ + and self.__task_callback__ is not None \ + and self.__is_running__ + finally: + self.__lock__.release() return result diff --git a/lib/simulated_values.py b/lib/simulated_values.py index bbe5b85..9999ee1 100644 --- a/lib/simulated_values.py +++ b/lib/simulated_values.py @@ -41,6 +41,9 @@ def simulate(self): self.value = lower_limit return self.__offset__ + self.value + + def get_value(self): + return self.__offset__ + self.value def __init__(self, rate, limit, initial_direction, initial_value=0.0, offset=0.0): self.__rate__ = rate diff --git a/lib/utilities.py b/lib/utilities.py index 5557c6e..5c19b19 100755 --- a/lib/utilities.py +++ b/lib/utilities.py @@ -150,7 +150,7 @@ def restart(): Restarts down the Pi. """ - if not local_debug.is_debug(): + if local_debug.IS_PI: subprocess.Popen(["sudo shutdown -r 30"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -161,7 +161,7 @@ def shutdown(seconds=30): Shuts down the Pi. """ - if not local_debug.is_debug(): + if local_debug.IS_PI: subprocess.Popen(["sudo shutdown -h {0}".format(int(seconds))], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/logging_object.py b/logging_object.py new file mode 100644 index 0000000..61c4dab --- /dev/null +++ b/logging_object.py @@ -0,0 +1,34 @@ +class LoggingObject(object): + """ + Gives basic logging capabilities to the inheriting objects. + Intended to be an abstract class. + """ + + def log(self, text): + """ + Logs the given text if a logger is available. + + Arguments: + text {string} -- The text to log + """ + + if self.__logger__ is not None: + self.__logger__.log_info_message(text) + else: + print(text) + + def warn(self, text): + """ + Logs the given text if a logger is available AS A WARNING. + + Arguments: + text {string} -- The text to log + """ + + if self.__logger__ is not None: + self.__logger__.log_warning_message(text) + else: + print(text) + + def __init__(self, logger): + self.__logger__ = logger diff --git a/media/ahrs_plus_adsb_view.jpg b/media/ahrs_plus_adsb_view.jpg index 7c9b145..7933814 100644 Binary files a/media/ahrs_plus_adsb_view.jpg and b/media/ahrs_plus_adsb_view.jpg differ diff --git a/media/ahrs_view.jpg b/media/ahrs_view.jpg index 14cf5ba..0e1debc 100644 Binary files a/media/ahrs_view.jpg and b/media/ahrs_view.jpg differ diff --git a/media/ahrs_view_old.jpg b/media/ahrs_view_old.jpg new file mode 100644 index 0000000..14cf5ba Binary files /dev/null and b/media/ahrs_view_old.jpg differ diff --git a/media/diagnostics_view.jpg b/media/diagnostics_view.jpg index 131999f..88e26a8 100644 Binary files a/media/diagnostics_view.jpg and b/media/diagnostics_view.jpg differ diff --git a/media/hud_logo.jpg b/media/hud_logo.jpg new file mode 100644 index 0000000..fe69ee2 Binary files /dev/null and b/media/hud_logo.jpg differ diff --git a/media/hud_logo.png b/media/hud_logo.png new file mode 100644 index 0000000..f507f4e Binary files /dev/null and b/media/hud_logo.png differ diff --git a/media/hud_logo.xcf b/media/hud_logo.xcf new file mode 100644 index 0000000..a78c501 Binary files /dev/null and b/media/hud_logo.xcf differ diff --git a/media/loading_view.jpg b/media/loading_view.jpg new file mode 100644 index 0000000..3eaf34d Binary files /dev/null and b/media/loading_view.jpg differ diff --git a/media/time_view.jpg b/media/time_view.jpg new file mode 100644 index 0000000..82aa7ac Binary files /dev/null and b/media/time_view.jpg differ diff --git a/media/traffic_listing_view.jpg b/media/traffic_listing_view.jpg index d4e57a1..e312265 100644 Binary files a/media/traffic_listing_view.jpg and b/media/traffic_listing_view.jpg differ diff --git a/media/traffic_view.jpg b/media/traffic_view.jpg index eb2f09c..04f91ed 100644 Binary files a/media/traffic_view.jpg and b/media/traffic_view.jpg differ diff --git a/readme.md b/readme.md index adfc716..893f14f 100755 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ This project aims to bring an affordable heads up display system into ANY cockpi The focus is to improve traffic awareness and to reduce the amount of time pilots reference tablets or an EFB. -_*NOTE:*_ This project relies on having a [Stratux](http://stratux.me/) build with AHRS and GPS. A suitable build can be done for less than \$150 (USD). +**NOTE:** This project relies on having a [Stratux](http://stratux.me/) build with AHRS and GPS. A suitable build can be done for less than $150 (USD). There are two versions that can be built: @@ -16,15 +16,17 @@ Using the "Kivic HUD 2nd Gen" projector and a Raspberry Pi 3. ![Kivic Version](media/kivic_in_flight.jpg) -Estimated cost is \$270 +Estimated cost is $270 -- \$40 for RaspberryPi 3 -- \$195 for Kivic 2nd Gen projector +- $40 for RaspberryPi 3 +- $195 for Kivic 2nd Gen projector - Fans, case, cables Uses 5V USB power. -_*NOTE:*_ This project initially used and reccomendedly the "HUDLY Classic" projector which is no longer available. +**NOTE:** This project initially used and reccomendedly the "HUDLY Classic" projector which is no longer available. + +**NOTE:** To have full functionality with a Stratux based unit, please use Stratux Version 1.4R5 or higher. ### Alternative, Less Expensive Version @@ -34,11 +36,11 @@ _NOTE:_ This version does have visibility issues in daylight conditions. Using a ![Teleprompter Glass Version In Flight](media/in_flight.jpg) -Estimated Cost is \$140 +Estimated Cost is $140 -- \$40 for a RaspberryPi 3 -- \$45 for the LCD screen -- \$20 for Teleprompter Glass and shipping. +- $40 for a RaspberryPi 3 +- $45 for the LCD screen +- $20 for Teleprompter Glass and shipping. - Cost of 3D printing the special case. - Cables @@ -48,17 +50,17 @@ Can be powered by a USB powerbank or USB power. You may use a number pad as input device. I used velcro to secure the number pad to my dashboard. -| Key | Action | -| --------- | ---------------------------------------------------------------------------- | -| Backspace | Tell the Stratux that you are in a level position. Resets the AHRS to level. | -| + | Next view | -| - | Previous view | -| = | Toggle rendering debug information | -| Esc | Send shutdown commands to both the HUD controller _*and*_ the Stratux | -| q | (_Full keyboard only_) Quit to the command line. | -| 0/Ins | Force a connection reset between the HUD and the Stratux | +Key | Action +--------- | ---------------------------------------------------------------------------- +Backspace | Tell the Stratux that you are in a level position. Resets the AHRS to level. ++ | Next view +- | Previous view += | Toggle rendering debug information +Esc | Send shutdown commands to both the HUD controller **and** the Stratux +q | (_Full keyboard only_) Quit to the command line. +0/Ins | Force a connection reset between the HUD and the Stratux -## Views +## Included (Default) Views - AHRS + ADS-B - Traffic @@ -70,18 +72,18 @@ You may use a number pad as input device. I used velcro to secure the number pad ### AHRS + ADS-B View -![Traffic View Screenshot](media/ahrs_plus_adsb_view.jpg) +![AHRS + ADS-B](media/ahrs_plus_adsb_view.jpg) This view shows attitude information along with targetting bugs that show the relative position and distance of traffic. In this example: -- There are five potential targets, all at a higher altitude. Three are relatively far away. The one directly behind us (far right hand side) is the closest. -- One of the five targets is within our field of view and has a targetting reticle. -- With are at a level pitch and roll. +- There are three (3) potential targets, all at a higher altitude. Two are relatively far away, one is closer. +- One of the targets is within our field of view and has a targetting reticle. +- With are rolled to the left slightly, less then 15 degrees. - We are 309 feet MSL. -- We are traveling forward a 0.4MPH (taxing) -- We have a GPS heading of 12, but do not have enough forward speed to obtain a heading from the AHRS chip. If the AHRS chip is unable to produce a reliable heading, `---` is shown for that portion of the heading. +- We are stopped, with a groundspeed of 0MPH +- We have a GPS heading of 236, but do not have enough forward speed to obtain a heading from the AHRS chip. If the AHRS chip is unable to produce a reliable heading, `---` is shown for that portion of the heading. _NOTE:_ This is the default view on startup. If you would like to switch to the `AHRS Only` You may press `-` on the keypad. @@ -91,32 +93,28 @@ _NOTE:_ This is the default view on startup. If you would like to switch to the This view shows a heading strip, target bugs, targetting reticles, and "information cards" about our potential traffic. -In this example, `N2803K` is almost directly ahead of us (middle of the screen). -The plane is 1.5 statute miles away, with a bearing of 51 degrees. We are currently on a heading of 012 degrees. The traffic is 500 feet above us. +In this example, `N2849K` is almost directly behind us (far left screen). The plane is 1.5 statute miles away, with a bearing of 70 degrees, and 100 feet above us. ### Traffic Listing View -![Traffic View Screenshot](media/traffic_listing_view.jpg) +![Traffic Listing View Screenshot](media/traffic_listing_view.jpg) This shows us _at most_ the eight closest planes. -The *IDENT(ifier will be the tail number when available, otherwise the IACO identifier or callsign may be used. -The *BEAR*ing is the heading to take to fly to that target. -The *DIST*ance is the distance to the target. -The *ALT\*itude is given in relative terms, with two digits dropped. +The *IDENT*ifier will be the tail number when available, otherwise the ICAO identifier or callsign may be used. The *BEAR*ing is the heading to take to fly to that target. The *DIST*ance is the distance to the target. The *ALT*itude is given in relative terms, with two digits dropped. -In this example, the closest target is QXE2382. We may see that plane if we looked out the cockpit at a heading of 276. The plane is only 1 statue mile away, and 11,200 feet above us. +In this example, the closest target is N1213S. The plane is only 1.2 statue mile away, and 1,500 feet above us. ### Diagnostics View -![Traffic View Screenshot](media/diagnostics_view.jpg) +![Diagnostics View Screenshot](media/diagnostics_view.jpg) -The diagnostics view is designed to help give some troubleshooting ability. -If a value is set for "OWNSHIP" (See the configuration file section), then any report from that tailnumber is ignored. -The IP addressis provided so you may use the configuration webpage if you set it up. +The diagnostics view is designed to help give some troubleshooting ability. If a value is set for "OWNSHIP" (See the configuration file section), then any report from that tailnumber is ignored. The IP address is provided so you may use the configuration webpage if you set it up. ### Universal Time +![Diagnostics View Screenshot](media/time_view.jpg) + Shows the current time in UTC at the bottom of the screen. ### Blank @@ -180,8 +178,7 @@ _NOTE:_ This _does not_ include a power source. You will need to supply ship pow #### Raspberry Pi 3B+ -If you are using a 3B+, it may suffer from undervoltage alerts. -These may be relieved by the following command to update your Linux install to the latest: +If you are using a 3B+, it may suffer from undervoltage alerts. These may be relieved by the following command to update your Linux install to the latest: ```bash sudo apt-get update && sudo apt-get dist-upgrade -y @@ -195,17 +192,19 @@ Make sure you are using a high quality power cable if you are using a Pi 3B+ 2. `cd ~` 3. `git clone https://github.com/JohnMarzulli/StratuxHud.git` 4. `cd StratuxHud` -5. `python --version`. Verify that your version is 2.7.14 -6. `sudo python setup.py develop` -7. `sudo raspi-config` -8. Choose "WiFi" again, and enter `stratux` as the SSID. No password. -9. `sudo vim /etc/wpa_supplicant/wpa_supplicant.conf` -10. Delete the section that contains your WiFi network, leaving the section that contains the Stratux network. -11. More info on configuring Linux WiFi: -12. Save and quit. -13. Type "crontab -e" -14. Select "Nano" (Option 1) -15. Enter the following text at the _bottom_ of the file: +5. `sudo apt-get install libgtk2.0-dev` a. Choose `Y` if prompted +6. `sudo cp ./media/hud_logo.png /usr/share/plymouth/themes/pix/splash.png` +7. `python --version`. Verify that your version is 2.7.14 +8. `sudo python setup.py develop` +9. `sudo raspi-config` +10. Choose "WiFi" again, and enter `stratux` as the SSID. No password. +11. `sudo vim /etc/wpa_supplicant/wpa_supplicant.conf` +12. Delete the section that contains your WiFi network, leaving the section that contains the Stratux network. +13. More info on configuring Linux WiFi: +14. Save and quit. +15. Type "crontab -e" +16. Select "Nano" (Option 1) +17. Enter the following text at the _bottom_ of the file: ```bash @reboot sudo python /home/pi/StratuxHud/stratux_hud.py & @@ -213,12 +212,18 @@ Make sure you are using a high quality power cable if you are using a Pi 3B+ 1. Save and quit. +### Ownship + +You may have the HUD ignore your own aircraft using a "OWNSHIP" functionality. The OWNSHIP value is set using the Stratux. The HUD retrieves the Mode S code set as the OWNSHIP and then filters out all reports so they are ignored. + +Please refer to the Stratux documentation on how to set the OWNSHIP value. + ### Kivic Based Setup 1. Install the Kivic projector per the Kivic directions. Please note that there is a release clip and the unit is removable. Also note that the combiner glass can be adjusted fore and aft. 2. Plug in the 3.5mm TRS cable between the Raspberry Pi and the Kivic. This is the same hole as the audio adapter for the Pi. 3. Plug the number pad into the Raspberry Pi. -4. You will need to run two Micro USB (5v) power cables. One to the HUD and one to the Raspberry Pi processing unit. These may be run from a battery bank, or from the ship's power _*if*_ you have 5V USB outlets. +4. You will need to run two Micro USB (5v) power cables. One to the HUD and one to the Raspberry Pi processing unit. These may be run from a battery bank, or from the ship's power **if** you have 5V USB outlets. 5. You may use the _optional_ sleeving to help keep the install tidy. ### Teleprompter Glass Based Setup @@ -234,22 +239,27 @@ Make sure you are using a high quality power cable if you are using a Pi 3B+ ### Revision History -| Date | Version | Major Changes | -| ---------- | ------- | ------------------------------------------------------------------------------------------------------------------ | -| 2019-03-31 | 1.4 | Add connection reset button. Fixes issues with the Diagnostic view running of of space. Initial port to Python 3.7 | -| 2019-01-31 | 1.3 | Improvements to the communication with the Stratux. Update directions for Kivic install. | -| 2018-10-13 | 1.2 | Major performance increases | -| 2018-09-07 | 1.1 | New system to allow views to be configurarable | -| 2018-07-17 | 1.0 | Initial release | +Date | Version | Major Changes +---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- +2019-06-30 | 1.5 | Support for the Aithre CO detector. New roll indicator. Various performance improvements. Visual warning if GPS is not plugged in. Use the OWNSHIP config from the receiver instead of local config. +2019-03-31 | 1.4 | Add connection reset button. Fixes issues with the Diagnostic view running of of space. Initial port to Python 3.7 +2019-01-31 | 1.3 | Improvements to the communication with the Stratux. Update directions for Kivic install. +2018-10-13 | 1.2 | Major performance increases +2018-09-07 | 1.1 | New system to allow views to be configurarable +2018-07-17 | 1.0 | Initial release ### Hardware Performance -| Board | Screen | Frames Per Second (AHRS View Only) | -| ------------------------------ | -------------- | ---------------------------------- | -| Rasp Pi 2 | Sun Founder 5" | ~25FPS to ~30FPS | -| Rasp Pi 3 (stand alone) | Kivic | Pending Retesting | -| Rasp Pi 3 (Running on Stratux) | Kivic | 30FPS | -| Rasp Pi 3B+ | Kivic | 50FPS | +Please note that performance characteristics are only shown for displays that are currently available for purchase. The Hudly Classic is intentionally not listed. + +Board | Screen | Frames Per Second (AHRS View Only) | Notes +------------------------------ | -------------- | ---------------------------------- | --------------- +Rasp Pi 2 | Sun Founder 5" | ~25FPS to ~30FPS | Not reccomended +Rasp Pi 3 (stand alone) | Kivic | 50FPS - 60FPS | Reccomended +Rasp Pi 3 (stand alone) | Hudly Wireless | 25FPS - 30FPS | Reccomended +Rasp Pi 3 (Running on Stratux) | Kivic | 30FPS | Not supported +Rasp Pi 3B+ | Kivic | 55FPS - 60FPS | Reccomended +Rasp Pi 3B+ | Hudly Wireless | 30FPS - 40FPS | Reccomended Please note that the frame rate is capped at 60FPS. Drawing any faster will not be detectable using the reccomended output systems. Reducing the framerate will reduce the powerdraw. @@ -259,6 +269,8 @@ This project uses the Liberation family of fonts. They can be found at +Many thanks to the Aithre team for providing the unit used to develop the plugin, and for their support in understanding the Aithre interface. + The following components are used: - Python @@ -271,4 +283,7 @@ The following components are used: This project is covered by the GPL v3 license. -Please see [LICENSE](LICENSE) +Please see + + + diff --git a/receiver_capabilities.py b/receiver_capabilities.py new file mode 100644 index 0000000..b9655bb --- /dev/null +++ b/receiver_capabilities.py @@ -0,0 +1,156 @@ +import datetime +import requests +from logging_object import LoggingObject + + +class StratuxCapabilities(LoggingObject): + """ + Get the capabilities of the Stratux, so we know what can be used + in the HUD. + """ + + def __get_value__(self, key): + """ + Gets the string value from the JSON, or None if it can't be found. + """ + if key is None: + return None + + if self.__capabilities_json__ is None: + return None + + if key in self.__capabilities_json__: + try: + return self.__capabilities_json__[key] + except KeyboardInterrupt: + raise + except SystemExit: + raise + except Exception as ex: + self.warn("__get_capability__ EX={}".format(ex)) + return None + + return None + + def __get_capability__(self, key): + """ + Returns a boolean from the json. + """ + if key is None: + return False + + if self.__capabilities_json__ is None: + return False + + if key in self.__capabilities_json__: + try: + return bool(self.__capabilities_json__[key]) + except KeyboardInterrupt: + raise + except SystemExit: + raise + except Exception as ex: + self.warn("__get_capability__ EX={}".format(ex)) + return False + + return False + + def __init__(self, stratux_address, stratux_session, logger=None, simulation_mode=False): + """ + Builds a list of Capabilities of the stratux. + """ + + super(StratuxCapabilities, self).__init__(logger) + + if stratux_address is None or simulation_mode: + self.__capabilities_json__ = None + self.traffic_enabled = False + self.gps_enabled = False + self.barometric_enabled = True + self.ahrs_enabled = True + self.ownship_mode_s = None + else: + url = "http://{0}/getSettings".format(stratux_address) + + try: + self.__capabilities_json__ = stratux_session.get( + url, timeout=2).json() + + except KeyboardInterrupt: + raise + except SystemExit: + raise + except Exception as ex: + self.__capabilities_json__ = {} + self.warn("EX in __init__ ex={}".format(ex)) + + self.traffic_enabled = self.__get_capability__('UAT_Enabled') + self.gps_enabled = self.__get_capability__('GPS_Enabled') + self.barometric_enabled = self.__get_capability__( + 'BMP_Sensor_Enabled') + self.ahrs_enabled = self.__get_capability__('IMU_Sensor_Enabled') + self.ownship_mode_s = self.__get_value__('OwnshipModeS') + + try: + # Ownship is in Hex... traffic reports come in int... + self.ownship_icao = int( + self.ownship_mode_s, 16) if self.ownship_mode_s is not None else 0 + except: + self.ownship_icao = 0 + # http://192.168.10.1/getSettings - get device settings. Example output: + # + # { + # "UAT_Enabled": true, + # "ES_Enabled": true, + # "Ping_Enabled": false, + # "GPS_Enabled": true, + # "BMP_Sensor_Enabled": true, + # "IMU_Sensor_Enabled": true, + # "NetworkOutputs": [ + # { + # "Conn": null, + # "Ip": "", + # "Port": 4000, + # "Capability": 5, + # "MessageQueueLen": 0, + # "LastUnreachable": "0001-01-01T00:00:00Z", + # "SleepFlag": false, + # "FFCrippled": false + # } + # ], + # "SerialOutputs": null, + # "DisplayTrafficSource": false, + # "DEBUG": false, + # "ReplayLog": false, + # "AHRSLog": false, + # "IMUMapping": [ + # 2, + # 0 + # ], + # "SensorQuaternion": [ + # 0.017336041263077348, + # 0.7071029888451218, + # 0.7068942365539764, + # -0.0023158510746434354 + # ], + # "C": [ + # -0.02794518875698111, + # 0.021365398113956116, + # -1.0051649525437176 + # ], + # "D": [ + # -0.43015839106418047, + # -0.0019837031159398175, + # -1.2866603595080415 + # ], + # "PPM": 0, + # "OwnshipModeS": "F00000", + # "WatchList": "", + # "DeveloperMode": false, + # "GLimits": "", + # "StaticIps": [], + # "WiFiSSID": "stratux", + # "WiFiChannel": 1, + # "WiFiSecurityEnabled": false, + # "WiFiPassphrase": "" + # } diff --git a/receiver_status.py b/receiver_status.py new file mode 100644 index 0000000..16be915 --- /dev/null +++ b/receiver_status.py @@ -0,0 +1,111 @@ +import datetime +import requests +from logging_object import LoggingObject + + +class StratuxStatus(LoggingObject): + """ + Object to hold retrieved status about the ADS-B reciever + """ + + def __get_status__(self, key): + if key is None: + return False + + if self.__status_json__ is None: + return False + + if key in self.__status_json__: + try: + return bool(self.__status_json__[key]) + except KeyboardInterrupt: + raise + except SystemExit: + raise + except Exception as ex: + self.warn("__get_status__ EX={}".format(ex)) + return False + + return False + + def __init__(self, stratux_address, stratux_session, logger, simulation_mode=False): + """ + Builds the ADS-B In receiver's status + """ + + super(StratuxStatus, self).__init__(logger) + + if stratux_address is None or simulation_mode: + self.__status_json__ = None + self.cpu_temp = 50.0 + self.satellites_locked = 0 + + else: + url = "http://{0}/getStatus".format(stratux_address) + + try: + self.__status_json__ = stratux_session.get( + url, timeout=2).json() + + except KeyboardInterrupt: + raise + except SystemExit: + raise + except Exception as ex: + self.warn("__get_status__ EX={}".format(ex)) + self.__status_json__ = {} + + self.cpu_temp = self.__get_status__('CPUTemp') + self.satellites_locked = self.__get_status__( + 'GPS_satellites_locked') + + # Results of a getStatus call + # { + # "Version": "v1.5b2", + # "Build": "8f4a52d7396c0dc20270e7644eebe5d9fc49eed9", + # "HardwareBuild": "", + # "Devices": 2, + # "Connected_Users": 1, + # "DiskBytesFree": 367050752, + # "UAT_messages_last_minute": 0, + # "UAT_messages_max": 38, + # "ES_messages_last_minute": 1413, + # "ES_messages_max": 6522, + # "UAT_traffic_targets_tracking": 0, + # "ES_traffic_targets_tracking": 5, + # "Ping_connected": false, + # "UATRadio_connected": false, + # "GPS_satellites_locked": 12, + # "GPS_satellites_seen": 13, + # "GPS_satellites_tracked": 19, + # "GPS_position_accuracy": 3, + # "GPS_connected": true, + # "GPS_solution": "GPS + SBAS (WAAS)", + # "GPS_detected_type": 55, + # "Uptime": 3261140, + # "UptimeClock": "0001-01-01T00:54:21.14Z", + # "CPUTemp": 49.925, + # "CPUTempMin": 44.546, + # "CPUTempMax": 55.843, + # "NetworkDataMessagesSent": 3080, + # "NetworkDataMessagesSentNonqueueable": 3080, + # "NetworkDataBytesSent": 89047, + # "NetworkDataBytesSentNonqueueable": 89047, + # "NetworkDataMessagesSentLastSec": 3, + # "NetworkDataMessagesSentNonqueueableLastSec": 3, + # "NetworkDataBytesSentLastSec": 84, + # "NetworkDataBytesSentNonqueueableLastSec": 84, + # "UAT_METAR_total": 0, + # "UAT_TAF_total": 0, + # "UAT_NEXRAD_total": 0, + # "UAT_SIGMET_total": 0, + # "UAT_PIREP_total": 0, + # "UAT_NOTAM_total": 0, + # "UAT_OTHER_total": 0, + # "Errors": [], + # "Logfile_Size": 90107, + # "AHRS_LogFiles_Size": 0, + # "BMPConnected": true, + # "IMUConnected": true, + # "NightMode": false + # } diff --git a/restful_host.py b/restful_host.py index 0ecab22..9f05f96 100644 --- a/restful_host.py +++ b/restful_host.py @@ -25,6 +25,7 @@ # EXAMPLES # Invoke-WebRequest -Uri "http://localhost:8080/settings" -Method GET -ContentType "application/json" # Invoke-WebRequest -Uri "http://localhost:8080/settings" -Method PUT -ContentType "application/json" -Body '{"flip_horizontal": true}' +# curl -X PUT -d '{"declination": 17}' http://localhost:8080/settings ERROR_JSON = '{success: false}' diff --git a/setup.py b/setup.py index 6f5017a..4f18f39 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,8 @@ installs = ['pytest', 'pygame', 'ws4py', - 'requests'] + 'requests', + 'bluepy'] setup(name='StratuxHud', diff --git a/traffic.py b/traffic.py index 0562556..758dfca 100755 --- a/traffic.py +++ b/traffic.py @@ -412,13 +412,19 @@ def get_traffic_with_position(self): self.__lock__.acquire() try: - traffic_with_position = {k: v for k, v in self.traffic.iteritems() if v is not None and v.is_valid_report( - ) and configuration.CONFIGURATION.ownship not in str(v.get_identifer())} - actionable_traffic = [self.traffic[identifier] - for identifier in traffic_with_position] + traffic_with_position = { + k: v for k, v in self.traffic.iteritems() + if v is not None and v.is_valid_report() + and configuration.CONFIGURATION.capabilities.ownship_icao != v.icao_address + } + except: + traffic_with_position = [] finally: self.__lock__.release() + actionable_traffic = [self.traffic[identifier] + for identifier in traffic_with_position] + sorted_traffic = sorted( actionable_traffic, key=lambda traffic: traffic.distance) @@ -495,7 +501,7 @@ def __init__(self, socket_address): self.hb = ws4py.websocket.Heartbeat(self) self.hb.start() - + def keep_alive(self): """ Sends the current date/time to the otherside of the socket @@ -722,12 +728,12 @@ def reset(self): AdsbTrafficClient.INSTANCE.shutdown() self.__last_action_time__ = datetime.datetime.utcnow() - AdsbTrafficClient.INSTANCE = AdsbTrafficClient(self.__socket_address__) + AdsbTrafficClient.INSTANCE = AdsbTrafficClient( + self.__socket_address__) AdsbTrafficClient.INSTANCE.run_in_background() finally: self.warn("Finished with WebSocket connection reset attempt.") - def shutdown(self): """ Shutsdown the WebSocket connection. @@ -784,7 +790,7 @@ def __is_connection_silently_timed_out__(self): msg_last_received_time = self.get_last_message_time() msg_last_received_delta = ( now - msg_last_received_time).total_seconds() - + # Do we want to include the ping as a reception time? # ping_last_received_time = self.get_last_ping_time() # ping_last_received_delta = ( @@ -793,7 +799,7 @@ def __is_connection_silently_timed_out__(self): connection_uptime = ( now - AdsbTrafficClient.INSTANCE.create_time).total_seconds() - if msg_last_received_delta > 60: # + if msg_last_received_delta > 60: # and ping_last_received_delta > 15: self.warn("{0:.1f} seconds connection uptime".format( connection_uptime)) diff --git a/units.py b/units.py index 4620b41..8012e56 100644 --- a/units.py +++ b/units.py @@ -116,7 +116,7 @@ def get_meters_per_second_from_mph(speed): return mph_to_ms * speed -def get_converted_units_string(units, distance, unit_type=DISTANCE): +def get_converted_units_string(units, distance, unit_type=DISTANCE, decimal_places=True): """ Given a base measurement (RAW from the ADS-B), a type of unit, and if it is speed or distance, returns a nice string for display. @@ -133,36 +133,55 @@ def get_converted_units_string(units, distance, unit_type=DISTANCE): >>> get_converted_units_string('statute', 0, DISTANCE) "0'" - >>> get_converted_units_string('statute', 0, SPEED) - '0.0MPH' + >>> get_converted_units_string('statute', 0.0, DISTANCE, True) + "0'" + >>> get_converted_units_string('statute', 0, SPEED, False) + '0 MPH' + >>> get_converted_units_string('statute', 0, SPEED, True) + '0 MPH' >>> get_converted_units_string('statute', 10, DISTANCE) "10'" >>> get_converted_units_string('statute', 5280, SPEED) - '1.0MPH' + '1 MPH' + >>> get_converted_units_string('statute', 5280, SPEED, False) + '1 MPH' + >>> get_converted_units_string('statute', 5280, SPEED, True) + '1 MPH' + >>> get_converted_units_string('statute', 5680, SPEED, True) + '1 MPH' >>> get_converted_units_string('statute', 528000, SPEED) - '100.0MPH' + '100 MPH' """ if units is None: units = STATUTE + + formatter_string = "{0:.1f}" + formatter_no_decimals = "{0:.0f}" + + if not decimal_places or unit_type is SPEED: + distance = int(distance) + formatter_string = formatter_no_decimals + + with_units_formatter = formatter_string + " {1}" if units != METRIC: if distance < IMPERIAL_NEARBY and unit_type != SPEED: - return "{0:.0f}".format(distance) + "'" + return formatter_no_decimals.format(distance) + "'" if units == NAUTICAL: - return "{0:.1f}{1}".format(distance / feet_to_nm, UNIT_LABELS[NAUTICAL][unit_type]) + return with_units_formatter.format(distance / feet_to_nm, UNIT_LABELS[NAUTICAL][unit_type]) - return "{0:.1f}{1}".format(distance / feet_to_sm, UNIT_LABELS[STATUTE][unit_type]) + return with_units_formatter.format(distance / feet_to_sm, UNIT_LABELS[STATUTE][unit_type]) else: conversion = distance / feet_to_km if conversion < 0.5 and units != SPEED: - return "{0:.1f}{1}".format(conversion, UNIT_LABELS[METRIC][unit_type]) + return with_units_formatter.format(conversion, UNIT_LABELS[METRIC][unit_type]) - return "{0:.1f}m".format(distance / feet_to_m) + return with_units_formatter.format(distance / feet_to_m, "m") - return "{0:.0f}'".format(distance) + return with_units_formatter.format(distance, "ft") if __name__ == '__main__': diff --git a/views.json b/views.json index c648fdb..6938d3f 100644 --- a/views.json +++ b/views.json @@ -9,6 +9,7 @@ "G Load", "Roll Indicator", "Groundspeed", + "Aithre", "ADSB Reticles", "ADSB Target Bugs Only" ], @@ -53,7 +54,8 @@ "Altitude", "G Load", "Roll Indicator", - "Groundspeed" + "Groundspeed", + "Aithre" ], "name": "AHRS Only" } diff --git a/views/adsb_element.py b/views/adsb_element.py index 3c0455b..1f99128 100644 --- a/views/adsb_element.py +++ b/views/adsb_element.py @@ -58,7 +58,7 @@ def __get_speed_string__(self, speed): return units.get_converted_units_string(speed_units, speed, units.SPEED) - def __get_distance_string__(self, distance): + def __get_distance_string__(self, distance, decimal_places=True): """ Gets the distance string for display using the units from the configuration. @@ -73,7 +73,7 @@ def __get_distance_string__(self, distance): display_units = configuration.CONFIGURATION.__get_config_value__( Configuration.DISTANCE_UNITS_KEY, units.STATUTE) - return units.get_converted_units_string(display_units, distance) + return units.get_converted_units_string(display_units, distance, decimal_places=decimal_places) def __get_traffic_projection__(self, orientation, traffic): """ diff --git a/views/adsb_traffic_listing.py b/views/adsb_traffic_listing.py index ca95ea4..ee415ed 100644 --- a/views/adsb_traffic_listing.py +++ b/views/adsb_traffic_listing.py @@ -64,7 +64,7 @@ def __get_padded_traffic_reports__(self, traffic_reports): def __get_report_text__(self, traffic): identifier = str(traffic.get_identifer()) altitude_delta = int(traffic.altitude / 100.0) - distance_text = self.__get_distance_string__(traffic.distance) + distance_text = self.__get_distance_string__(traffic.distance, True) delta_sign = '' if altitude_delta > 0: delta_sign = '+' diff --git a/views/altitude.py b/views/altitude.py index 40e6cf3..b0e064b 100644 --- a/views/altitude.py +++ b/views/altitude.py @@ -1,12 +1,12 @@ +from ahrs_element import AhrsElement +from lib.task_timer import TaskTimer +from numbers import Number +import lib.display as display import pygame import testing testing.load_imports() -import lib.display as display -from lib.task_timer import TaskTimer -from ahrs_element import AhrsElement - class Altitude(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): @@ -15,13 +15,16 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y - text_half_height - self.__rhs__ = int(framebuffer_size[0]) # was 0.9 + self.__rhs__ = int(framebuffer_size[0]) # was 0.9 def render(self, framebuffer, orientation): self.task_timer.start() - altitude_text = str(int(orientation.alt)) + "' MSL" + altitude_text = str(int(orientation.alt)) + \ + "' MSL" if orientation.alt is not None and isinstance( + orientation.alt, Number) else "INOP" + color = display.WHITE if orientation.alt is not None and orientation.gps_online else display.RED alt_texture = self.__font__.render( - altitude_text, True, display.WHITE, display.BLACK) + altitude_text, True, color, display.BLACK) text_width, text_height = alt_texture.get_size() framebuffer.blit( diff --git a/views/compass_and_heading_bottom_element.py b/views/compass_and_heading_bottom_element.py index 1831fb3..0c2bc16 100644 --- a/views/compass_and_heading_bottom_element.py +++ b/views/compass_and_heading_bottom_element.py @@ -1,3 +1,7 @@ +from numbers import Number +import hud_elements +from lib.task_timer import TaskTimer +from lib.display import * import pygame from compass_and_heading_top_element import CompassAndHeadingTopElement @@ -6,10 +10,6 @@ import utils testing.load_imports() -from lib.display import * -from lib.task_timer import TaskTimer -import hud_elements - class CompassAndHeadingBottomElement(CompassAndHeadingTopElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): @@ -20,8 +20,10 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size self.__line_bottom__ = framebuffer_size[1] self.heading_text_y = self.__line_top__ - (font.get_height() * 1.2) + self._heading_box_y_ = framebuffer_size[1] - \ + int(font.get_height() * 2.8) self.compass_text_y = framebuffer_size[1] - \ - int(font.get_height() * 2) + int(font.get_height()) self.__border_width__ = 4 text_height = font.get_height() border_vertical_size = (text_height >> 1) + (text_height >> 2) @@ -30,12 +32,13 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size half_width = int(self.__heading_text__[360][1][0] * 3.5) self.__heading_text_box_lines__ = [ [self.__center_x__ - half_width, - self.compass_text_y - border_vertical_size + vertical_alignment_offset], + self._heading_box_y_ - border_vertical_size + vertical_alignment_offset], [self.__center_x__ + half_width, - self.compass_text_y - border_vertical_size + vertical_alignment_offset], + self._heading_box_y_ - border_vertical_size + vertical_alignment_offset], [self.__center_x__ + half_width, - self.compass_text_y + border_vertical_size + vertical_alignment_offset], - [self.__center_x__ - half_width, self.compass_text_y + border_vertical_size + vertical_alignment_offset]] + self._heading_box_y_ + border_vertical_size + vertical_alignment_offset], + [self.__center_x__ - half_width, + self._heading_box_y_ + border_vertical_size + vertical_alignment_offset]] def __render_heading_mark__(self, framebuffer, x_pos, heading): pygame.draw.line(framebuffer, GREEN, @@ -43,7 +46,7 @@ def __render_heading_mark__(self, framebuffer, x_pos, heading): self.__render_heading_text__( framebuffer, - utils.apply_declination(heading), + heading, x_pos, self.compass_text_y) @@ -59,30 +62,19 @@ def render(self, framebuffer, orientation): heading = orientation.get_onscreen_projection_heading() - if heading < 0: - heading += 360 - - if heading > 360: - heading -= 360 - - [self.__render_heading_mark__(framebuffer, heading_mark_to_render[0], heading_mark_to_render[1]) - for heading_mark_to_render in self.__heading_strip__[heading]] - - # Render the text that is showing our AHRS and GPS headings - heading_text = "{0} | {1}".format( - utils.apply_declination( - orientation.get_onscreen_projection_display_heading()), - utils.apply_declination(orientation.gps_heading)) + if isinstance(heading, Number): + if heading < 0: + heading += 360 - rendered_text = self.__font__.render( - heading_text, True, BLACK, GREEN) - text_width, text_height = rendered_text.get_size() + if heading > 360: + heading -= 360 - pygame.draw.polygon(framebuffer, GREEN, - self.__heading_text_box_lines__) + [self.__render_heading_mark__(framebuffer, heading_mark_to_render[0], heading_mark_to_render[1]) + for heading_mark_to_render in self.__heading_strip__[heading]] - framebuffer.blit( - rendered_text, (self.__center_x__ - (text_width >> 1), self.compass_text_y)) + self._render_hallow_heading_box_(orientation, + framebuffer, + self._heading_box_y_) self.task_timer.stop() diff --git a/views/compass_and_heading_top_element.py b/views/compass_and_heading_top_element.py index daabc64..e87490c 100644 --- a/views/compass_and_heading_top_element.py +++ b/views/compass_and_heading_top_element.py @@ -1,13 +1,13 @@ +from numbers import Number +from ahrs_element import AhrsElement +from lib.task_timer import TaskTimer +import hud_elements +import lib.display as display import pygame import utils import testing testing.load_imports() -import lib.display as display -import hud_elements -from lib.task_timer import TaskTimer -from ahrs_element import AhrsElement - class CompassAndHeadingTopElement(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): @@ -69,8 +69,8 @@ def __generate_heading_strip__(self, heading): to_the_left = (heading - heading_strip) to_the_right = (heading + heading_strip) - displayed_left = utils.apply_declination(to_the_left) - displayed_right = utils.apply_declination(to_the_right) + displayed_left = to_the_left + displayed_right = to_the_right if to_the_left < 0: to_the_left += 360 @@ -118,32 +118,40 @@ def render(self, framebuffer, orientation): for heading_mark_to_render in self.__heading_strip__[heading]] # Render the text that is showing our AHRS and GPS headings - cover_old_rendering_spaces = " " - heading_text = "{0}{1} | {2}{0}".format(cover_old_rendering_spaces, - str(int(utils.apply_declination( - orientation.get_onscreen_projection_display_heading()))).rjust(3), - str(int(utils.apply_declination(orientation.gps_heading))).rjust(3)) + heading_y_pos = self.__font__.get_height() << 1 + self._render_hallow_heading_box_(orientation, + framebuffer, + heading_y_pos) + self.task_timer.stop() + + def _render_hallow_heading_box_(self, orientation, framebuffer, heading_y_pos): + heading_text = "{0} | {1}".format( + str(utils.apply_declination( + orientation.get_onscreen_projection_display_heading())).rjust(3), + str(utils.apply_declination( + orientation.get_onscreen_gps_heading())).rjust(3)) rendered_text = self.__font__.render( - heading_text, True, display.GREEN, display.BLACK) + heading_text, True, display.GREEN) text_width, text_height = rendered_text.get_size() framebuffer.blit( - rendered_text, (self.__center_x__ - (text_width >> 1), text_height << 1)) + rendered_text, (self.__center_x__ - (text_width >> 1), heading_y_pos)) pygame.draw.lines(framebuffer, display.GREEN, True, self.__heading_text_box_lines__, 2) - self.task_timer.stop() def __render_heading_text__(self, framebuffer, heading, position_x, position_y): """ Renders the text with the results centered on the given position. """ - rendered_text, half_size = self.__heading_text__[heading] + if isinstance(heading, Number): + heading = int(heading) + rendered_text, half_size = self.__heading_text__[heading] - framebuffer.blit( - rendered_text, (position_x - half_size[0], position_y - half_size[1])) + framebuffer.blit( + rendered_text, (position_x - half_size[0], position_y - half_size[1])) if __name__ == '__main__': diff --git a/views/groundspeed.py b/views/groundspeed.py index 21cb6b5..1202c11 100644 --- a/views/groundspeed.py +++ b/views/groundspeed.py @@ -1,14 +1,14 @@ +import configuration +from ahrs_element import AhrsElement +import units +from lib.task_timer import TaskTimer +import lib.display as display +from numbers import Number import pygame import testing testing.load_imports() -from lib.display import * -from lib.task_timer import TaskTimer -import units -from ahrs_element import AhrsElement -import configuration - class Groundspeed(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): @@ -29,10 +29,12 @@ def render(self, framebuffer, orientation): configuration.Configuration.DISTANCE_UNITS_KEY, units.STATUTE) groundspeed_text = units.get_converted_units_string( - speed_units, orientation.groundspeed * units.feet_to_nm, units.SPEED) + speed_units, orientation.groundspeed * units.feet_to_nm, unit_type=units.SPEED, decimal_places=False) if orientation.groundspeed is not None and isinstance(orientation.groundspeed, Number) else "INOP" + + display_color = display.WHITE if orientation is not None and orientation.groundspeed is not None and orientation.gps_online else display.RED texture = self.__font__.render( - groundspeed_text, True, WHITE, BLACK) + groundspeed_text, True, display_color, display.BLACK) framebuffer.blit( texture, (self.__left_x__, self.__text_y_pos__)) diff --git a/views/roll_indicator.py b/views/roll_indicator.py index 0e68073..5e76415 100644 --- a/views/roll_indicator.py +++ b/views/roll_indicator.py @@ -1,18 +1,20 @@ +from ahrs_element import AhrsElement +from lib.task_timer import TaskTimer +import lib.display as display import math import pygame +import pygame.gfxdraw import testing testing.load_imports() -from lib.display import * -from lib.task_timer import TaskTimer -from ahrs_element import AhrsElement +TWO_PI = 2.0 * math.pi -class RollIndicator(AhrsElement): +class RollIndicatorText(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): - self.task_timer = TaskTimer('RollIndicator') + self.task_timer = TaskTimer('RollIndicatorText') self.__roll_elements__ = {} self.__framebuffer_size__ = framebuffer_size self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) @@ -22,7 +24,7 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size for reference_angle in range(-180, 181): text = font.render( - "{0:3}".format(int(math.fabs(reference_angle))), True, WHITE, BLACK) + "{0:3}".format(int(math.fabs(reference_angle))), True, display.WHITE, display.BLACK) size_x, size_y = text.get_size() self.__roll_elements__[reference_angle] = ( text, (size_x >> 1, size_y >> 1)) @@ -37,7 +39,7 @@ def render(self, framebuffer, orientation): attitude_text = "{0}{1:3} | {2:3}".format(pitch_direction, pitch, roll) roll_texture = self.__font__.render( - attitude_text, True, BLACK, WHITE) + attitude_text, True, display.BLACK, display.WHITE) texture_size = roll_texture.get_size() text_half_width, text_half_height = texture_size text_half_width = int(text_half_width / 2) @@ -46,6 +48,104 @@ def render(self, framebuffer, orientation): self.task_timer.stop() +def wrap_angle(angle): + """ + Wraps an angle (degrees) to be between 0.0 and 360 + Arguments: + angle {float} -- The input angle + Returns: and value that is between 0 and 360, inclusive. + """ + + if angle < -360.0: + return wrap_angle(angle + 360.0) + + if angle > 360.0: + return wrap_angle(angle - 360.0) + + return angle + + +def wrap_radians(radians): + """ + Wraps an angle that is in radians to be between 0.0 and 2Pi + Arguments: + angle {float} -- The input angle + Returns: and value that is between 0 and 2Pi, inclusive. + """ + if radians < 0.0: + return wrap_radians(radians + TWO_PI) + + if radians > TWO_PI: + return wrap_angle(radians - TWO_PI) + + return radians + + +class RollIndicator(AhrsElement): + def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): + self.task_timer = TaskTimer('RollIndicator') + self.__framebuffer_size__ = framebuffer_size + self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) + half_texture_height = int(font.get_height()) >> 1 + self.__font__ = font + self.__text_y_pos__ = self.__center__[1] - half_texture_height + self.arc_radius = int(framebuffer_size[1] / 3) + self.top_arc_squash = 0.75 + self.arc_angle_adjust = math.pi / 8.0 + self.roll_indicator_arc_radians = 0.03 + self.arc_box = [self.__center__[0] - self.arc_radius, self.__center__[1] - ( + self.arc_radius / 2), self.arc_radius * 2, (self.arc_radius * 2) * self.top_arc_squash] + self.reference_line_size = 20 + self.reference_arc_box = [self.arc_box[0], self.arc_box[1] - self.reference_line_size, + self.arc_box[2], self.arc_box[3] - self.reference_line_size] + self.smaller_reference_arc_box = [self.arc_box[0], self.arc_box[1] - ( + self.reference_line_size/2), self.arc_box[2], self.arc_box[3] - (self.reference_line_size/2)] + self.half_pi = math.pi / 2.0 + + def render(self, framebuffer, orientation): + self.task_timer.start() + + roll_in_radians = math.radians(orientation.roll) + + # Draws the reference arc + pygame.draw.arc(framebuffer, + display.GREEN, + self.arc_box, + self.arc_angle_adjust, + math.pi - self.arc_angle_adjust, + 4) + + # Draw the important reference angles + for roll_angle in [-30, -15, 15, 30]: + reference_roll_in_radians = math.radians(roll_angle + 90.0) + pygame.draw.arc(framebuffer, + display.GREEN, + self.smaller_reference_arc_box, + reference_roll_in_radians - self.roll_indicator_arc_radians, + reference_roll_in_radians + self.roll_indicator_arc_radians, + self.reference_line_size / 2) + + # Draw the REALLY important reference angles longer + for roll_angle in [-90, -60, -45, 0, 45, 60, 90]: + reference_roll_in_radians = math.radians(roll_angle + 90.0) + pygame.draw.arc(framebuffer, + display.GREEN, + self.reference_arc_box, + reference_roll_in_radians - self.roll_indicator_arc_radians, + reference_roll_in_radians + self.roll_indicator_arc_radians, + self.reference_line_size) + + # Draws the current roll + pygame.draw.arc(framebuffer, + display.YELLOW, + self.arc_box, + self.half_pi - roll_in_radians - self.roll_indicator_arc_radians, + self.half_pi - roll_in_radians + self.roll_indicator_arc_radians, + self.reference_line_size * 2) + + self.task_timer.stop() + + if __name__ == '__main__': import hud_elements hud_elements.run_ahrs_hud_element(RollIndicator, False) diff --git a/views/skid_and_gs.py b/views/skid_and_gs.py index 5a2cea7..e44eeb5 100644 --- a/views/skid_and_gs.py +++ b/views/skid_and_gs.py @@ -3,7 +3,7 @@ import testing testing.load_imports() -from lib.display import * +import lib.display as display from lib.task_timer import TaskTimer from ahrs_element import AhrsElement @@ -20,9 +20,9 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size def render(self, framebuffer, orientation): self.task_timer.start() - g_load_text = "{0:.1f}Gs".format(orientation.g_load) + g_load_text = "{0:.1f} Gs".format(orientation.g_load) texture = self.__font__.render( - g_load_text, True, WHITE, BLACK) + g_load_text, True, display.WHITE, display.BLACK) text_width, text_height = texture.get_size() framebuffer.blit( diff --git a/views/system_info.py b/views/system_info.py index da24b04..1f2db55 100644 --- a/views/system_info.py +++ b/views/system_info.py @@ -1,6 +1,7 @@ import commands from traffic import AdsbTrafficClient from ahrs_element import AhrsElement +import aithre import configuration import units import lib.local_debug as local_debug @@ -31,7 +32,7 @@ def get_ip_address(): """ try: - if not local_debug.is_debug(): + if local_debug.IS_LINUX and local_debug.IS_PI: ip_addr = commands.getoutput('hostname -I').strip() return (ip_addr, GREEN) else: @@ -67,19 +68,59 @@ def get_cpu_temp(): color = GREEN try: - # if not local_debug.is_debug(): - raspberry_pi_temp = open('/sys/class/thermal/thermal_zone0/temp') - temp = float(raspberry_pi_temp.read()) - temp = temp/1000 - # else: - # temp = float(datetime.datetime.utcnow().second) + 30.0 + if local_debug.IS_LINUX: + linux_cpu_temp = open('/sys/class/thermal/thermal_zone0/temp') + temp = float(linux_cpu_temp.read()) + temp = temp/1000 - color = get_cpu_temp_text_color(temp) + color = get_cpu_temp_text_color(temp) - return ("{0}C".format(int(math.floor(temp))), color) + return ("{0}C".format(int(math.floor(temp))), color) except: return ('---', GRAY) + return ('---', GRAY) + + +def get_aithre_co_color(co_ppm): + """ + Returns the color code for the carbon monoxide levels + + Arguments: + co_ppm {int} -- Integer containing the Parts Per Million of CO + + Returns: + color -- The color to display + """ + color = BLUE + + if co_ppm > aithre.CO_WARNING: + color = RED + elif co_ppm > aithre.CO_SAFE: + color = YELLOW + + return color + + +def get_aithre_battery_color(battery_percent): + """ + Returns the color code for the Aithre battery level. + + Arguments: + battery_percent {int} -- The percentage of battery. + + Returns: + color -- The color to show the battery percentage in. + """ + color = RED + + if battery_percent >= aithre.BATTERY_SAFE: + color = GREEN + elif battery_percent >= aithre.BATTERY_WARNING: + color = YELLOW + + return color + def get_websocket_uptime(): """ @@ -120,9 +161,7 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size self.task_timer = TaskTimer('Time') self.__font__ = font self.font_height = font.get_height() - text_half_height = int(self.font_height) >> 1 - self.__text_y_pos__ = framebuffer_size[1] - \ - text_half_height - self.font_height + self.__text_y_pos__ = framebuffer_size[1] - self.font_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = int(framebuffer_size[0] * 0.01) @@ -132,6 +171,46 @@ def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size self.__ip_address__ = get_ip_address() self.__cpu_temp__ = None self.__framebuffer_size__ = framebuffer_size + self.__line_spacing__ = 1.01 + + def __get_aithre_text_and_color__(self): + """ + Gets the text and text color for the Aithre status. + """ + + if aithre.sensor is None: + return ('DISCONNECTED', RED) if configuration.CONFIGURATION.aithre_enabled else ('DISABLED', BLUE) + + battery_text = 'UNK' + battery_color = RED + + try: + battery = aithre.sensor.get_battery() + battery_suffix = "%" + if isinstance(battery, basestring): + battery_suffix = "" + if battery is not None: + battery_color = get_aithre_battery_color(battery) + battery_text = "bat:{}{}".format(battery, battery_suffix) + except Exception as ex: + battery_text = 'ERR' + + co_text = 'UNK' + co_color = RED + + try: + co_ppm = aithre.sensor.get_co_level() + + if co_ppm is not None: + co_text = 'co:{}ppm'.format(co_ppm) + co_color = get_aithre_co_color(co_ppm) + except Exception as ex: + co_text = 'ERR' + + color = RED if co_color is RED or battery_color is RED else \ + (YELLOW if co_color is YELLOW or battery_color is YELLOW else BLUE) + + return ('{} {}'.format(co_text, battery_text), color) def render(self, framebuffer, orientation): self.task_timer.start() @@ -140,28 +219,32 @@ def render(self, framebuffer, orientation): if self.__update_ip_timer__ <= 0: self.__ip_address__ = get_ip_address() self.__update_ip_timer__ = 120 - + self.__update_temp_timer__ -= 1 if self.__update_temp_timer__ <= 0: self.__cpu_temp__ = get_cpu_temp() self.__update_temp_timer__ = 60 - info_lines = [["VERSION : ", [configuration.VERSION, YELLOW]]] - + info_lines = [["VERSION : ", [configuration.VERSION, YELLOW]], + ["DECLINATION : ", [str(configuration.CONFIGURATION.get_declination()), BLUE]]] addresses = self.__ip_address__[0].split(' ') for addr in addresses: info_lines.append( ["IP : ", (addr, self.__ip_address__[1])]) + info_lines.append( + ["AITHRE : ", self.__get_aithre_text_and_color__()]) + # Status lines are pushed in as a stack. # First line in the array is at the bottom. # Last line in the array is towards the top. info_lines.append(["HUD CPU : ", self.__cpu_temp__]) info_lines.append(["SOCKET : ", get_websocket_uptime()]) - info_lines.append(["DECLINATION : ", [str(configuration.CONFIGURATION.get_declination()), BLUE]]) - info_lines.append(["OWNSHIP : ", [configuration.CONFIGURATION.ownship, BLUE]]) - info_lines.append(["DISPLAY RES : ", ["{} x {}".format(self.__framebuffer_size__[0], self.__framebuffer_size__[1]), BLUE]]) + info_lines.append( + ["OWNSHIP : ", ["{}/{}".format(configuration.CONFIGURATION.capabilities.ownship_mode_s, configuration.CONFIGURATION.capabilities.ownship_icao), BLUE]]) + info_lines.append(["DISPLAY RES : ", ["{} x {}".format( + self.__framebuffer_size__[0], self.__framebuffer_size__[1]), BLUE]]) render_y = self.__text_y_pos__ @@ -176,11 +259,51 @@ def render(self, framebuffer, orientation): line[1][0], True, line[1][1], BLACK) framebuffer.blit(texture_rhs, (size[0], render_y)) - render_y = render_y - (self.font_height * 1.1) + render_y = render_y - (self.font_height * self.__line_spacing__) self.task_timer.stop() +class Aithre(AhrsElement): + def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): + self.task_timer = TaskTimer('Aithre') + self.__font__ = font + center_y = framebuffer_size[1] >> 2 + text_half_height = int(font.get_height()) >> 1 + self.__text_y_pos__ = center_y - text_half_height + self.__lhs__ = 0 + self.__has_been_connected__ = False + + def render(self, framebuffer, orientation): + self.task_timer.start() + + if aithre.sensor is not None and configuration.CONFIGURATION.aithre_enabled: + co_level = aithre.sensor.get_co_level() + + if co_level is None or isinstance(co_level, basestring): + if self.__has_been_connected__: + co_color = RED + co_ppm_text = "OFFLINE" + else: + self.task_timer.stop() + return + else: + co_color = get_aithre_co_color(co_level) + co_ppm_text = str(int(co_level)) + " PPM" + self.__has_been_connected__ = True + + co_ppm_texture = self.__font__.render( + co_ppm_text, True, co_color, BLACK) + + framebuffer.blit( + co_ppm_texture, (self.__lhs__, self.__text_y_pos__)) + self.task_timer.stop() + + +if __name__ == '__main__': + import hud_elements + hud_elements.run_ahrs_hud_element(Aithre) + if __name__ == '__main__': import hud_elements diff --git a/views/utils.py b/views/utils.py index a95c13d..c334ab2 100644 --- a/views/utils.py +++ b/views/utils.py @@ -22,9 +22,9 @@ def apply_declination(heading): return heading if new_heading < 0.0: - new_heading = new_heading + 360.0 + new_heading = new_heading + 360 if new_heading > 360.0: - new_heading = new_heading - 360.0 + new_heading = new_heading - 360 return new_heading