01234567890123456789012345678901234567890123456789012345678901234567890123456789
12 345678910111213141516171819202122 7778798081828384858687888990919293949596 979899100101102103104105106107108109110111112113114115116 199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 18881889189018911892189318941895189618971898189919001901190219031904190519061907 1908190919101911191219131914191519161917191819191920192119221923192419251926 19271928192919301931193219331934 19351936193719381939194019411942 1943 19441945194619471948194919501951195219531954195519561957195819591960196119621963 64466447644864496450645164526453645464556456645764586459646064616462646364646465 64666467646864696470647164726473647464756476647764786479648064816482648364846485 66136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653 6702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780 73377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377 | #!/usr/bin/python3 import datetime import gc import io import json import math import multiprocessing import numbers import os import pickle import queue import re import shutil import signal import statistics import sys import textwrap import time import tracemalloc import types <----SKIPPED LINES----> # dropped from the nearby list PERSISTENCE_SECONDS = 300 TRUNCATE = 50 # max number of keys to include in a histogram image file # number of seconds to pause between each radio poll / command processing loop LOOP_DELAY_SECONDS = 1 # number of seconds to wait between recording heartbeats to the status file HEARTBEAT_SECONDS = 10 # version control directory CODE_REPOSITORY = '' VERSION_REPOSITORY = 'versions/' VERSION_WEBSITE_PATH = VERSION_REPOSITORY VERSION_MESSAGEBOARD = None VERSION_ARDUINO = None # histogram logic truncates to exactly 30 days of hours MAX_INSIGHT_HORIZON_DAYS = 31 # This file is where the radio drops its json file DUMP_JSON_FILE = '/run/readsb/aircraft.json' # At the time a flight is first identified as being of interest (in that # it falls within MIN_METERS meters of HOME), it - and core attributes # derived from FlightAware, if any - is appended to the end of this pickle # file. However, since this file is cached in working memory, flights older # than 30 days are flushed from this periodically. PICKLE_FLIGHTS = 'pickle/flights.pk' # This allows us to identify the full history (including what was last sent # to the splitflap display in a programmatic fashion. While it may be # interesting in its own right, its real use is to handle the "replay" # button, so we know to enable it if what is displayed is the last flight. PICKLE_SCREENS = 'pickle/screens.pk' # Status data about messageboard - is it running, etc. Specifically, has tuples # of data (timestamp, system_id, status), where system_id is either the pin id # of GPIO, or a 0 to indicate overall system, and status is boolean PICKLE_DASHBOARD = 'pickle/dashboard.pk' CACHED_ELEMENT_PREFIX = 'cached_' <----SKIPPED LINES----> FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 FLAG_INSIGHT_FIRST_AIRCRAFT = 9 FLAG_INSIGHT_LONGEST_DELAY = 10 FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11 FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12 FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13 FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14 FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15 FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16 FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17 FLAG_INSIGHT_HOUR_DELAY_TIME = 18 FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19 FLAG_INSIGHT_DATE_DELAY_TIME = 20 FLAG_INSIGHT_HELICOPTER = 21 INSIGHT_TYPES = 22 TEMP_FAN_TURN_ON_CELSIUS = 65 TEMP_FAN_TURN_OFF_CELSIUS = 50 TEMPERATURE_LOG_FREQUENCY_SECONDS = 30 TEMPERATURE_LOG = 'secure/temp.csv' # GPIO relay connections # format: (GPIO pin, true message, false message, relay number, # description, initial_state) GPIO_ERROR_VESTABOARD_CONNECTION = ( 22, 'ERROR: Vestaboard unavailable', 'SUCCESS: Vestaboard available', 1, 'Vestaboard connected', False) GPIO_ERROR_FLIGHT_AWARE_CONNECTION = ( 23, 'ERROR: FlightAware not available', 'SUCCESS: FlightAware available', 2, 'FlightAware connected', False) GPIO_ERROR_ARDUINO_SERVO_CONNECTION = ( 24, 'ERROR: Servos not running or lost connection', 'SUCCESS: Handshake with servo Arduino received', 3, 'Hemisphere connected', True) GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = ( <----SKIPPED LINES----> MEMORY_DIRECTORY = MESSAGEBOARD_PATH + MEMORY_DIRECTORY PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_DASHBOARD LOGFILE = MESSAGEBOARD_PATH + LOGFILE PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE PICKLE_SCREENS = MESSAGEBOARD_PATH + PICKLE_SCREENS CODE_REPOSITORY = MESSAGEBOARD_PATH HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE STDERR_FILE = WEBSERVER_PATH + STDERR_FILE BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE UPTIMES_FILE = WEBSERVER_PATH + UPTIMES_FILE CODE_HISTORY_FILE = WEBSERVER_PATH + CODE_HISTORY_FILE NEW_AIRCRAFT_FILE = WEBSERVER_PATH + NEW_AIRCRAFT_FILE TEMPERATURE_LOG = WEBSERVER_PATH + TEMPERATURE_LOG HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML HOURLY_IMAGE_FILE = ( WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE) VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY TIMEZONE = 'US/Pacific' # timezone of display TZ = pytz.timezone(TIMEZONE) # iata codes that we don't need to expand KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK') SPLITFLAP_CHARS_PER_LINE = 22 SPLITFLAP_LINE_COUNT = 6 DIRECTIONS_4 = ['N', 'E', 'S', 'W'] DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'] DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] <----SKIPPED LINES----> # just simply need to continue updating this list to keep the # dictionary up to date (i.e.: we don't need to directly touch the # flights dictionary in main). (last_seen, current_path) = persistent_path.get(id_to_use, (None, [])) if ( # flight position has been updated with this radio signal not current_path or simplified_aircraft.get('lat') != current_path[-1].get('lat') or simplified_aircraft.get('lon') != current_path[-1].get('lon')): current_path.append(simplified_aircraft) persistent_path[id_to_use] = (now, current_path) # if the flight was last seen too far in the past, remove the track info for f in list(persistent_path.keys()): (last_seen, current_path) = persistent_path[f] if last_seen < now - PERSISTENCE_SECONDS: persistent_path.pop(f) return (nearby_aircraft, now, json_desc_dict, persistent_path) last_query_time = 0 def GetFlightAwareJson(flight_number): """Scrapes the text json message from FlightAware for a given flight number. Given a flight number, loads the corresponding FlightAware webpage for that flight and extracts the relevant script that contains all the flight details from that page. But only queries at most once per fixed period of time so as to avoid being blocked. Args: flight_number: text flight number (i.e.: SWA1234) Returns: Two tuple: - Text representation of the json message from FlightAware. - Text string of error message, if any """ min_query_delay_seconds = 90 url = 'https://flightaware.com/live/flight/' + flight_number global last_query_time seconds_since_last_query = time.time() - last_query_time if last_query_time and seconds_since_last_query < min_query_delay_seconds: error_msg = ( 'Unable to query FA for URL since last query to FA was only %d seconds ' 'ago; min of %d seconds needed: %s' % ( seconds_since_last_query, min_query_delay_seconds, url)) return '', error_msg last_query_time = time.time() try: response = requests.get(url, timeout=5) except requests.exceptions.RequestException as e: error_msg = 'Unable to query FA for URL due to %s: %s' % (e, url) return '', error_msg soup = bs4.BeautifulSoup(response.text, 'html.parser') l = soup.find_all('script') flight_script = None for script in l: if 'trackpollBootstrap' in str(script): flight_script = str(script) break if not flight_script: error_msg = ( 'Unable to find trackpollBootstrap script in page: ' + response.text) Log(error_msg) return '', error_msg first_open_curly_brace = flight_script.find('{') last_close_curly_brace = flight_script.rfind('}') flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1] return flight_json, '' def Unidecode(s): """Convert a special unicode characters to closest ASCII representation.""" <----SKIPPED LINES----> Args: s: String to publish. local_key: string key from Vestaboard for local API access. local_address: the address and port to the local Vestaboard service. timeout: Max duration in seconds that we should wait to establish a connection. update_dashboard: Boolean indicating whether this method should update the system dashboard, or if that should be left to the calling function. Returns: False if successful; error message string if failure occurs. """ error_code = False data = str(StringToCharArray(s)) headers = { 'X-Vestaboard-Local-Api-Key': local_key, 'Content-Type': 'application/x-www-form-urlencoded', } try: response = requests.post( local_address, headers=headers, data=data, timeout=timeout) except requests.exceptions.RequestException as e: error_msg = 'Unable to reach %s (%s): %s' % (local_address, response, e) Log(error_msg) error_code = True if not error_code: Log('Message sent to Vestaboard by way of local api: %s' % s) if update_dashboard: UpdateStatusLight( GPIO_ERROR_VESTABOARD_CONNECTION, error_code, error_msg) return error_code def CurlTimingDetailsToString(curl): """Extracts timing details of a curl request into a readable string.""" timing = {} <----SKIPPED LINES----> del message_queue[:] else: # display only one message, being mindful of the display timing messages_to_display = [message_queue.pop(0)] for message in messages_to_display: # we cannot just unpack the tuple because messages of type # FLAG_MSG_FLIGHT are 3-tuples (with the third element being the flight # dictionary) whereas other message types are 2-tuples message_type = message[0] message_text = message[1] # There may be one or several insight messages that were added to the # message queue along with the flight at a time when the screen was # enabled, but by the time it comes to display them, the screen is now # disabled. These should not be displayed. Note that this check only # needs to be done for insight messages because other message types # are user initiated and so presumably should be displayed irrespective # of when the user triggered it to be displayed. if message_type == FLAG_MSG_INSIGHT and not MessageMeetsDisplayCriteria( configuration): Log('Message %s purged') else: if isinstance(message_text, str): message_text = textwrap.wrap( message_text, width=SPLITFLAP_CHARS_PER_LINE) display_message = Screenify(message_text, False) Log(display_message, file=ALL_MESSAGE_FILE) # Saving this to disk allows us to identify # persistently whats currently on the screen PickleObjectToFile(message, PICKLE_SCREENS, True) screens.append(message) MaintainRollingWebLog(display_message, 25) if not SIMULATION: splitflap_message = Screenify(message_text, True) PublishMessageWeb(splitflap_message) next_message_time = time.time() + configuration['setting_delay'] <----SKIPPED LINES----> if n/25 == int(n/25): print(' - %d' % n) CreateFlightInsights( flights[:n+1], configuration.get('insights', 'hide'), {}) PickleObjectToFile(flight, tmp_f, False) if mtime == os.path.getmtime(f): shutil.move(tmp_f, f) else: print('Aborted: failed to bootstrap %s: file changed while in process' % full_path) return def ResetLogs(config): """Clears the non-scrolling logs if reset_logs in config.""" if 'reset_logs' in config: removed_files = [] for f in ( STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE, NEW_AIRCRAFT_FILE, TEMPERATURE_LOG): if RemoveFile(f): removed_files.append(f) open(f, 'a').close() Log('Reset logs: cleared files %s' % '; '.join(removed_files)) config.pop('reset_logs') config = BuildSettings(config) WriteFile(CONFIG_FILE, config) return config def CheckTemperature(configuration, last_logged=0): """Turn on fan if temperature exceeds threshold. Args: configuration: dictionary of configuration settings. last_logged: epoch at which temperature was last logged. Returns: Epoch at which temperature was last logged. """ if RASPBERRY_PI: temperature = gpiozero.CPUTemperature().temperature if temperature > TEMP_FAN_TURN_ON_CELSIUS: UpdateStatusLight(GPIO_FAN, True, 'Temperature: %.1f' % temperature) elif temperature < TEMP_FAN_TURN_OFF_CELSIUS: UpdateStatusLight(GPIO_FAN, False) now = time.time() if (configuration.get('log_temperature') and now - last_logged > TEMPERATURE_LOG_FREQUENCY_SECONDS): line = ','.join([ str(now), EpochDisplayTime(now, '%Y-%m-%d'), EpochDisplayTime(now, '%H:%M:%S'), str(temperature)]) with open(TEMPERATURE_LOG, 'a') as f: f.write('%s\n' % line) last_logged = now return last_logged pin_values = {} # caches last set value def SetPinMode(): """Initialize output GPIO pins for output on Raspberry Pi.""" global pin_values if RASPBERRY_PI: RPi.GPIO.setmode(RPi.GPIO.BCM) pins = ( GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION, GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2) for pin in pins: initial_state = pin[5] pin_values[pin[0]] = initial_state # Initialize state of pins UpdateDashboard(initial_state, pin) <----SKIPPED LINES----> RemoveFile(HISTOGRAM_CONFIG_FILE) # We also need to make sure there are flights on which to generate a # histogram! Why might there not be any flights? Primarily during a # simulation, if there's a lingering histogram file at the time of # history restart. if histogram and not flights: Log('Histogram requested (%s) but no flights in memory' % histogram) if histogram and flights: message_queue.extend(TriggerHistograms(flights, histogram)) if message_queue: # Any personal message displayed has been cleared personal_message = None # check time & if appropriate, display next message from queue next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration, screen_history) reboot = CheckRebootNeeded( startup_time, message_queue, json_desc_dict, configuration) temp_last_logged = CheckTemperature(configuration, temp_last_logged) if not SIMULATION: time.sleep(max(0, next_loop_time - time.time())) next_loop_time = time.time() + LOOP_DELAY_SECONDS else: SIMULATION_COUNTER += 1 if simulation_slowdown: SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft) # now that we've completed the loop, lets potentially dump the # memory snapshot iteration += 1 # this completes the iteration-th time thru the loop if initial_memory_dump: DumpMemorySnapsnot( configuration, iteration, startup_time, initial_frame_count) if SIMULATION: SimulationEnd(message_queue, flights, screen_history) PerformGracefulShutdown(shutdown, reboot) <----SKIPPED LINES----> |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
1234567891011121314151617181920212223 78798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 203204205206207208209210211212213214215216217218219220221222 223224225226227228229230231232233234235236237238239240241242 286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 18901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993 64766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516 66446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684 67336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767 676867696770677167726773677467756776677767786779 67806781678267836784678567866787678867896790679167926793679467956796679767986799 73567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396 | #!/usr/bin/python3 import csv import datetime import gc import io import json import math import multiprocessing import numbers import os import pickle import queue import re import shutil import signal import statistics import sys import textwrap import time import tracemalloc import types <----SKIPPED LINES----> # dropped from the nearby list PERSISTENCE_SECONDS = 300 TRUNCATE = 50 # max number of keys to include in a histogram image file # number of seconds to pause between each radio poll / command processing loop LOOP_DELAY_SECONDS = 1 # number of seconds to wait between recording heartbeats to the status file HEARTBEAT_SECONDS = 10 # version control directory CODE_REPOSITORY = '' VERSION_REPOSITORY = 'versions/' VERSION_WEBSITE_PATH = VERSION_REPOSITORY VERSION_MESSAGEBOARD = None VERSION_ARDUINO = None # histogram logic truncates to exactly 30 days of hours MAX_INSIGHT_HORIZON_DAYS = 31 # This file is where the radio drops its json file DUMP_JSON_FILE = '/run/readsb/aircraft.json' # This file is where the query history to the flight aware webpage goes FLIGHTAWARE_HISTORY_FILE = 'secure/flightaware.txt' # At the time a flight is first identified as being of interest (in that # it falls within MIN_METERS meters of HOME), it - and core attributes # derived from FlightAware, if any - is appended to the end of this pickle # file. However, since this file is cached in working memory, flights older # than 30 days are flushed from this periodically. PICKLE_FLIGHTS = 'pickle/flights.pk' # This allows us to identify the full history (including what was last sent # to the splitflap display in a programmatic fashion. While it may be # interesting in its own right, its real use is to handle the "replay" # button, so we know to enable it if what is displayed is the last flight. PICKLE_SCREENS = 'pickle/screens.pk' # Status data about messageboard - is it running, etc. Specifically, has tuples # of data (timestamp, system_id, status), where system_id is either the pin id # of GPIO, or a 0 to indicate overall system, and status is boolean PICKLE_DASHBOARD = 'pickle/dashboard.pk' CACHED_ELEMENT_PREFIX = 'cached_' <----SKIPPED LINES----> FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 FLAG_INSIGHT_FIRST_AIRCRAFT = 9 FLAG_INSIGHT_LONGEST_DELAY = 10 FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11 FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12 FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13 FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14 FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15 FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16 FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17 FLAG_INSIGHT_HOUR_DELAY_TIME = 18 FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19 FLAG_INSIGHT_DATE_DELAY_TIME = 20 FLAG_INSIGHT_HELICOPTER = 21 INSIGHT_TYPES = 22 TEMP_FAN_TURN_ON_CELSIUS = 65 TEMP_FAN_TURN_OFF_CELSIUS = 50 # GPIO relay connections # format: (GPIO pin, true message, false message, relay number, # description, initial_state) GPIO_ERROR_VESTABOARD_CONNECTION = ( 22, 'ERROR: Vestaboard unavailable', 'SUCCESS: Vestaboard available', 1, 'Vestaboard connected', False) GPIO_ERROR_FLIGHT_AWARE_CONNECTION = ( 23, 'ERROR: FlightAware not available', 'SUCCESS: FlightAware available', 2, 'FlightAware connected', False) GPIO_ERROR_ARDUINO_SERVO_CONNECTION = ( 24, 'ERROR: Servos not running or lost connection', 'SUCCESS: Handshake with servo Arduino received', 3, 'Hemisphere connected', True) GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = ( <----SKIPPED LINES----> MEMORY_DIRECTORY = MESSAGEBOARD_PATH + MEMORY_DIRECTORY PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_DASHBOARD LOGFILE = MESSAGEBOARD_PATH + LOGFILE PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE PICKLE_SCREENS = MESSAGEBOARD_PATH + PICKLE_SCREENS CODE_REPOSITORY = MESSAGEBOARD_PATH HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE STDERR_FILE = WEBSERVER_PATH + STDERR_FILE BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE UPTIMES_FILE = WEBSERVER_PATH + UPTIMES_FILE CODE_HISTORY_FILE = WEBSERVER_PATH + CODE_HISTORY_FILE NEW_AIRCRAFT_FILE = WEBSERVER_PATH + NEW_AIRCRAFT_FILE FLIGHTAWARE_HISTORY_FILE = WEBSERVER_PATH + FLIGHTAWARE_HISTORY_FILE HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML HOURLY_IMAGE_FILE = ( WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE) VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY TIMEZONE = 'US/Pacific' # timezone of display TZ = pytz.timezone(TIMEZONE) # iata codes that we don't need to expand KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK') SPLITFLAP_CHARS_PER_LINE = 22 SPLITFLAP_LINE_COUNT = 6 DIRECTIONS_4 = ['N', 'E', 'S', 'W'] DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'] DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] <----SKIPPED LINES----> # just simply need to continue updating this list to keep the # dictionary up to date (i.e.: we don't need to directly touch the # flights dictionary in main). (last_seen, current_path) = persistent_path.get(id_to_use, (None, [])) if ( # flight position has been updated with this radio signal not current_path or simplified_aircraft.get('lat') != current_path[-1].get('lat') or simplified_aircraft.get('lon') != current_path[-1].get('lon')): current_path.append(simplified_aircraft) persistent_path[id_to_use] = (now, current_path) # if the flight was last seen too far in the past, remove the track info for f in list(persistent_path.keys()): (last_seen, current_path) = persistent_path[f] if last_seen < now - PERSISTENCE_SECONDS: persistent_path.pop(f) return (nearby_aircraft, now, json_desc_dict, persistent_path) def LogFlightAwareQuery(flight_number, error_message=''): """Log whenever we are about to attempt a query on FlightAware. To help troubleshoot the apparent frequency of FlightAware queries, we will log when we *desire* to make the queries (and for what flights), irrespective of whether such queries are throttled before the attempt, successful, or fail for some other reason. Specifically, we will log in a dedicated text file three fields: - the timestamp (epoch) - the flight number - the failure message, if any """ with open(FLIGHTAWARE_HISTORY_FILE, 'a', newline='') as csv_file: writer = csv.writer( csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) writer.writerow([time.time(), flight_number, error_message]) last_query_time = 0 def GetFlightAwareJson(flight_number): """Scrapes the text json message from FlightAware for a given flight number. Given a flight number, loads the corresponding FlightAware webpage for that flight and extracts the relevant script that contains all the flight details from that page. But only queries at most once per fixed period of time so as to avoid being blocked. Args: flight_number: text flight number (i.e.: SWA1234) Returns: Two tuple: - Text representation of the json message from FlightAware. - Text string of error message, if any """ min_query_delay_seconds = 90 url = 'https://flightaware.com/live/flight/' + flight_number # It seems there are a lot of queries on flight aware that come at # about the same time, so let's see if we can track when and why they # happen LogFlightAwareQuery(flight_number) global last_query_time seconds_since_last_query = time.time() - last_query_time if last_query_time and seconds_since_last_query < min_query_delay_seconds: error_msg = ( 'Unable to query FA for URL since last query to FA was only %d seconds ' 'ago; min of %d seconds needed: %s' % ( seconds_since_last_query, min_query_delay_seconds, url)) LogFlightAwareQuery(flight_number, error_msg) return '', error_msg last_query_time = time.time() try: response = requests.get(url, timeout=5) except requests.exceptions.RequestException as e: error_msg = 'Unable to query FA for URL due to %s: %s' % (e, url) LogFlightAwareQuery(flight_number, error_msg) return '', error_msg LogFlightAwareQuery(flight_number) soup = bs4.BeautifulSoup(response.text, 'html.parser') l = soup.find_all('script') flight_script = None for script in l: if 'trackpollBootstrap' in str(script): flight_script = str(script) break if not flight_script: error_msg = ( 'Unable to find trackpollBootstrap script in page: ' + response.text) Log(error_msg) return '', error_msg first_open_curly_brace = flight_script.find('{') last_close_curly_brace = flight_script.rfind('}') flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1] return flight_json, '' def Unidecode(s): """Convert a special unicode characters to closest ASCII representation.""" <----SKIPPED LINES----> Args: s: String to publish. local_key: string key from Vestaboard for local API access. local_address: the address and port to the local Vestaboard service. timeout: Max duration in seconds that we should wait to establish a connection. update_dashboard: Boolean indicating whether this method should update the system dashboard, or if that should be left to the calling function. Returns: False if successful; error message string if failure occurs. """ error_code = False data = str(StringToCharArray(s)) headers = { 'X-Vestaboard-Local-Api-Key': local_key, 'Content-Type': 'application/x-www-form-urlencoded', } response = 'Unable to complete requests.post' try: response = requests.post( local_address, headers=headers, data=data, timeout=timeout) except requests.exceptions.RequestException as e: error_msg = 'Unable to reach %s (%s): %s' % (local_address, response, e) Log(error_msg) error_code = True if not error_code: Log('Message sent to Vestaboard by way of local api: %s' % s) if update_dashboard: UpdateStatusLight( GPIO_ERROR_VESTABOARD_CONNECTION, error_code, error_msg) return error_code def CurlTimingDetailsToString(curl): """Extracts timing details of a curl request into a readable string.""" timing = {} <----SKIPPED LINES----> del message_queue[:] else: # display only one message, being mindful of the display timing messages_to_display = [message_queue.pop(0)] for message in messages_to_display: # we cannot just unpack the tuple because messages of type # FLAG_MSG_FLIGHT are 3-tuples (with the third element being the flight # dictionary) whereas other message types are 2-tuples message_type = message[0] message_text = message[1] # There may be one or several insight messages that were added to the # message queue along with the flight at a time when the screen was # enabled, but by the time it comes to display them, the screen is now # disabled. These should not be displayed. Note that this check only # needs to be done for insight messages because other message types # are user initiated and so presumably should be displayed irrespective # of when the user triggered it to be displayed. if message_type == FLAG_MSG_INSIGHT and not MessageMeetsDisplayCriteria( configuration): Log('Message %s purged' % message_text) else: if isinstance(message_text, str): message_text = textwrap.wrap( message_text, width=SPLITFLAP_CHARS_PER_LINE) display_message = Screenify(message_text, False) Log(display_message, file=ALL_MESSAGE_FILE) # Saving this to disk allows us to identify # persistently whats currently on the screen PickleObjectToFile(message, PICKLE_SCREENS, True) screens.append(message) MaintainRollingWebLog(display_message, 25) if not SIMULATION: splitflap_message = Screenify(message_text, True) PublishMessageWeb(splitflap_message) next_message_time = time.time() + configuration['setting_delay'] <----SKIPPED LINES----> if n/25 == int(n/25): print(' - %d' % n) CreateFlightInsights( flights[:n+1], configuration.get('insights', 'hide'), {}) PickleObjectToFile(flight, tmp_f, False) if mtime == os.path.getmtime(f): shutil.move(tmp_f, f) else: print('Aborted: failed to bootstrap %s: file changed while in process' % full_path) return def ResetLogs(config): """Clears the non-scrolling logs if reset_logs in config.""" if 'reset_logs' in config: removed_files = [] for f in ( STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE, NEW_AIRCRAFT_FILE, FLIGHTAWARE_HISTORY_FILE): if RemoveFile(f): removed_files.append(f) open(f, 'a').close() Log('Reset logs: cleared files %s' % '; '.join(removed_files)) config.pop('reset_logs') config = BuildSettings(config) WriteFile(CONFIG_FILE, config) return config def CheckTemperature(last_logged=0): """Turn on fan if temperature exceeds threshold. Args: last_logged: epoch at which temperature was last logged. Returns: Epoch at which temperature was last logged. """ if RASPBERRY_PI: temperature = gpiozero.CPUTemperature().temperature if temperature > TEMP_FAN_TURN_ON_CELSIUS: UpdateStatusLight(GPIO_FAN, True, 'Temperature: %.1f' % temperature) elif temperature < TEMP_FAN_TURN_OFF_CELSIUS: UpdateStatusLight(GPIO_FAN, False) return last_logged pin_values = {} # caches last set value def SetPinMode(): """Initialize output GPIO pins for output on Raspberry Pi.""" global pin_values if RASPBERRY_PI: RPi.GPIO.setmode(RPi.GPIO.BCM) pins = ( GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION, GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2) for pin in pins: initial_state = pin[5] pin_values[pin[0]] = initial_state # Initialize state of pins UpdateDashboard(initial_state, pin) <----SKIPPED LINES----> RemoveFile(HISTOGRAM_CONFIG_FILE) # We also need to make sure there are flights on which to generate a # histogram! Why might there not be any flights? Primarily during a # simulation, if there's a lingering histogram file at the time of # history restart. if histogram and not flights: Log('Histogram requested (%s) but no flights in memory' % histogram) if histogram and flights: message_queue.extend(TriggerHistograms(flights, histogram)) if message_queue: # Any personal message displayed has been cleared personal_message = None # check time & if appropriate, display next message from queue next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration, screen_history) reboot = CheckRebootNeeded( startup_time, message_queue, json_desc_dict, configuration) temp_last_logged = CheckTemperature(temp_last_logged) if not SIMULATION: time.sleep(max(0, next_loop_time - time.time())) next_loop_time = time.time() + LOOP_DELAY_SECONDS else: SIMULATION_COUNTER += 1 if simulation_slowdown: SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft) # now that we've completed the loop, lets potentially dump the # memory snapshot iteration += 1 # this completes the iteration-th time thru the loop if initial_memory_dump: DumpMemorySnapsnot( configuration, iteration, startup_time, initial_frame_count) if SIMULATION: SimulationEnd(message_queue, flights, screen_history) PerformGracefulShutdown(shutdown, reboot) <----SKIPPED LINES----> |