01234567890123456789012345678901234567890123456789012345678901234567890123456789
1617181920212223242526272829303132333435363738394041424344454647484950515253545556 157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 188189190191192193194195196197198199200201202203204205206207 281282283284285286287288289290291292293294295296297298299300301302303304305306 307308 309310311312313314315316317318319320321322323324325326327328 15251526152715281529153015311532153315341535153615371538153915401541154215431544 15451546154715481549155015511552155315541555155615571558155915601561156215631564 15741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632 16331634163516361637163816391640164116421643164416451646164716481649165016511652 19171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939 194019411942194319441945194619471948194919501951195219531954 1955195619571958195919601961196219631964 1965196619671968196919701971197219731974197519761977 1978197919801981 19821983198419851986198719881989199019911992199319941995199619971998199920002001 56965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739 63936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415 64166417641864196420642164226423642464256426642764286429643064316432643364346435 6437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488 6489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525 6526 65276528 652965306531653265336534653565366537653865396540654165426543654465456546 6547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571 66986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738 731173127313731473157316731773187319732073217322732373247325732673277328732973307331 73327333733473357336733773387339734073417342734373447345734673477348734973507351 74127413741474157416741774187419742074217422742374247425742674277428742974307431 74327433743474357436743774387439744074417442744374447445744674477448744974507451 | <----SKIPPED LINES----> import statistics import sys import textwrap import time import tracemalloc import types import bs4 import dateutils import numpy import matplotlib import matplotlib.pyplot import psutil import pycurl import pytz import requests import tzlocal import unidecode # pylint: disable=line-too-long from constants import RASPBERRY_PI, MESSAGEBOARD_PATH, WEBSERVER_PATH, KEY, SECRET, SUBSCRIPTION_ID, LOCAL_KEY, LOCAL_VESTABOARD_ADDRESS # pylint: enable=line-too-long import arduino if RASPBERRY_PI: import gpiozero # pylint: disable=E0401 import RPi.GPIO # pylint: disable=E0401 VERBOSE = False # additional messages logged SHUTDOWN_SIGNAL = '' REBOOT_SIGNAL = False # to be tracked in the dashboard messages so that we know when a # restart due to exit (vs. a long delay in some processing) happened INSTANCE_START_TIME = time.time() SIMULATION = False SIMULATION_COUNTER = 0 <----SKIPPED LINES----> # always leveraged by the running software. CONFIG_FILE = 'secure/settings.txt' CONFIG_BOOLEANS = ( 'setting_screen_enabled', 'next_flight', 'reset_logs', 'log_jsons') # A few key settings for the messageboard are its sensitivity to displaying # flights - though it logs all flights within range, it may not be desirable # to display all flights to the user. Two key parameters are the maximum # altitude, and the furthest away we anticipate the flight being at its # closest point to HOME. As those two parameters are manipulated in the # settings, a histogram is displayed with one or potentially two series, # showing the present and potentially prior-set distribution of flights, # by hour throughout the day, over the last seven days, normalized to # flights per day. This allows those parameters to be fine-tuned in a # useful way. This file is the location, on the webserver, of that image, # which needs to be in alignment with the html page that displays it. HOURLY_IMAGE_FILE = 'hours.png' # This is all messages that have been sent to the board since the last time # the file was manually cleared. Newest messages are at the bottom. It is # visible at the webserver. #enumeration of all messages sent to board ALL_MESSAGE_FILE = 'secure/all_messages.txt' # This shows the most recent n messages sent to the board. Newest messages # are at the top for easier viewing of "what did I miss". ROLLING_MESSAGE_FILE = 'rolling_messages.txt' STDERR_FILE = 'secure/stderr.txt' BACKUP_FILE = 'secure/backup.txt' SERVICE_VERIFICATION_FILE = 'secure/service-verification.txt' UPTIMES_FILE = 'uptimes.php' CODE_HISTORY_FILE = 'code_history.php' # This keeps a log of all aircraft not yet cataloged in the AIRCRAFT_LENGTH dict NEW_AIRCRAFT_FILE = 'secure/new_aircraft.txt' FLAG_MSG_FLIGHT = 1 # basic flight details FLAG_MSG_INSIGHT = 2 # random tidbit about a flight FLAG_MSG_HISTOGRAM = 3 # histogram message FLAG_MSG_CLEAR = 4 # a blank message to clear the screen # user-entered message to display for some duration of time FLAG_MSG_PERSONAL = 5 FLAG_INSIGHT_LAST_SEEN = 0 FLAG_INSIGHT_DIFF_AIRCRAFT = 1 FLAG_INSIGHT_NTH_FLIGHT = 2 FLAG_INSIGHT_GROUNDSPEED = 3 FLAG_INSIGHT_ALTITUDE = 4 FLAG_INSIGHT_VERTRATE = 5 FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 <----SKIPPED LINES----> ' function gtag(){dataLayer.push(arguments);}\n' " gtag('js', new Date());\n" " gtag('config', 'UA-99931533-2');\n" '</script>\n') #if running on raspberry, then need to prepend path to file names if RASPBERRY_PI: MEMORY_DIRECTORY = MESSAGEBOARD_PATH + MEMORY_DIRECTORY PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_SYSTEM_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_SYSTEM_DASHBOARD PICKLE_FLIGHT_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_FLIGHT_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 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----> persistent_path: dictionary where keys are flight numbers, and the values are a sequential list of the location-attributes in the json file; allows for tracking the flight path over time. log_jsons: boolean indicating whether we should pickle the JSONs. flights: list of flight dictionaries; if no json is returned, used to find a recent flight with same flight number to augment this flight with origin / destination / airline. Returns: A tuple: - updated persistent_nearby_aircraft - (possibly empty) dictionary of flight attributes of the new flight upon its first observation. - the time of the radio observation if present; None if no radio dump - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest observed flight, etc.) - persistent_path, a data structure containing past details of a flight's location as described in ParseDumpJson - a text message indicating any errors in querying FlightAware or populating flight details - timestamp indicating exact time at which FlightAware was queried (or attempted to be queried), if a query was made in this pass """ flight_details = {} error_message = '' now = time.time() if SIMULATION: (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER] else: dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True) # Often there is no flight aware query, so we need to give a default value flight_aware_timestamp = 0 json_desc_dict = {} current_nearby_aircraft = {} if dump_json: (current_nearby_aircraft, now, json_desc_dict, persistent_path) = ParseDumpJson( dump_json, persistent_path) <----SKIPPED LINES----> # newly_nearby_flight_identifiers is a list of 2-tuples, where each # tuple is (flight_number, squawk) newly_nearby_flight_identifiers_str = ', '.join( ['%s/%s ' % (*i,) for i in newly_nearby_flight_identifiers]) newly_nearby_flight_details_str = '\n'.join([ str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers]) Log('Multiple newly-nearby flights: %s\n%s' % ( newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str)) flight_identifier = newly_nearby_flight_identifiers[0] flight_aware_json = {} if SIMULATION: json_times = [j[1] for j in FA_JSONS] if json_time in json_times: flight_aware_json = FA_JSONS[json_times.index(json_time)][0] flight_aware_timestamp = time.time() # "real" timestamp unavailable elif flight_identifier[0]: flight_number = flight_identifier[0] flight_aware_json, error_message, flight_aware_timestamp = ( GetFlightAwareJson(flight_number)) if flight_aware_json: UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False) else: failure_message = 'No json from Flightaware for flight %s: %s' % ( flight_number, error_message[:500]) Log(failure_message) UpdateStatusLight( GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True, failure_message) flight_details = {} if flight_aware_json: flight_details = ParseFlightAwareJson(flight_aware_json) elif flight_identifier[0]: # if there's a flight number but no json flight_details, derived_attr_msg = FindAttributesFromSimilarFlights( flight_identifier[0], flights) error_message = '%s; %s' % (error_message, derived_attr_msg) if not SIMULATION and log_jsons: PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True) # Augment FlightAware details with radio / radio-derived details flight_details.update(current_nearby_aircraft[flight_identifier]) # Augment with the past location data; the [1] is because recall that # persistent_path[key] is actually a 2-tuple, the first element being # the most recent time seen, and the second element being the actual # path. But we do not need to keep around the most recent time seen any # more. flight_details['persistent_path'] = persistent_path[flight_identifier][1] return ( persistent_nearby_aircraft, flight_details, now, json_desc_dict, persistent_path, error_message, flight_aware_timestamp) def DescribeDumpJson(parsed): """Generates dict with descriptive attributes about the dump json file. Args: parsed: The parsed json file. Returns: Dictionary with attributes about radio range, number of flights seen, etc. """ json_desc_dict = {} json_desc_dict['now'] = parsed['now'] aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS] json_desc_dict['radio_range_flights'] = len(aircraft) aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a] current_distances = [HaversineDistanceMeters( <----SKIPPED LINES----> (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: Three tuple: - Text representation of the json message from FlightAware. - Text string of error message, if any - Timestamp of attempted query on FlightAware """ 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 %s at %s since last query to FA was only' ' %d seconds ago; min of %d seconds needed: %s' % ( flight_number, EpochDisplayTime(time.time(), format_string='%H:%M:%S'), seconds_since_last_query, min_query_delay_seconds, url)) return '', error_msg, time.time() last_query_time = time.time() try: response = requests.get(url, timeout=5) query_time = time.time() except requests.exceptions.RequestException as e: query_time = time.time() # did not get to the query_time assignment above error_msg = 'Unable to query FA for URL due to %s: %s' % (e, url) return '', error_msg, query_time 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, query_time 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, '', query_time def Unidecode(s): """Convert a special unicode characters to closest ASCII representation.""" if s is not None: s = unidecode.unidecode(s) return s def FindAttributesFromSimilarFlights(this_flight_number, flights): """Returns a dictionary with info about a flight based on other flights. We may not get a json from the internet about this flight for any number of reasons: internet down; website down; too frequent queries; etc. However, there are still some basic attributes we can derive about this flight from past observations of the same flight number, or past observations about the flight number prefix. Specifically, we can get the flight's airline, origin, and destination. Args: <----SKIPPED LINES----> f.close() def SimulationSetup(): """Updates global variable file names and loads JSON data for simulations.""" # Clear file so that shell tail -f process can continue to point to same file def ClearFile(filename): if os.path.exists(filename): with open(filename, 'w') as f: f.write('') global SIMULATION SIMULATION = True global DUMP_JSONS DUMP_JSONS = UnpickleObjectFromFile(PICKLE_DUMP_JSON_FILE, True) global FA_JSONS FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True) global ALL_MESSAGE_FILE ALL_MESSAGE_FILE = PrependFileName(ALL_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ALL_MESSAGE_FILE) global LOGFILE LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX) ClearFile(LOGFILE) global ROLLING_LOGFILE ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX) ClearFile(ROLLING_LOGFILE) global ROLLING_MESSAGE_FILE ROLLING_MESSAGE_FILE = PrependFileName( ROLLING_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ROLLING_MESSAGE_FILE) global PICKLE_FLIGHTS PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX) filenames = UnpickleObjectFromFile( PICKLE_FLIGHTS, True, max_days=None, filenames=True) for file in filenames: ClearFile(file) <----SKIPPED LINES----> The message is pushed to the vestaboard splitflap display by way of its web services; see https://docs.vestaboard.com/introduction for more details; if the web service fails, it then reattempts to publish using the local api. TODO: rewrite to use the easier-to-follow requests library, more in line with PublishMessageLocal. Args: s: String to publish. subscription_id: string subscription id from Vestaboard. key: string key from Vestaboard. secret: string secret from Vestaboard. timeout: Max duration in seconds that we should wait to establish a connection. Returns: Two-tuple: - Text string indicating how the message was displayed (web or local api), including error messages encountered if any. - Status code which is one of the text strings SUCCESS, WARNING, or FAIL, indicating whether the web service was used (SUCCESS), the local service was used because the web service failed (WARNING), or both failed (FAIL). """ error_code = False curl = pycurl.Curl() # See https://stackoverflow.com/questions/31826814/ # curl-post-request-into-pycurl-code # Set URL value curl.setopt( pycurl.URL, 'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id) curl.setopt(pycurl.HTTPHEADER, [ 'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret]) curl.setopt(pycurl.TIMEOUT_MS, timeout*1000) curl.setopt(pycurl.POST, 1) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean # preparing body the way pycurl.READDATA wants it body_as_dict = {'characters': StringToCharArray(s)} <----SKIPPED LINES----> body_as_file_object = io.StringIO(body_as_json_string) # prepare and send. See also: pycurl.READFUNCTION to pass function instead curl.setopt(pycurl.READDATA, body_as_file_object) curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string)) web_publish_failure_msg = '' try: curl.perform() status_msg = 'Web service published' publish_status_code = 'SUCCESS' except pycurl.error as e: timing_message = CurlTimingDetailsToString(curl) web_publish_failure_msg = ( 'curl.perform() failed with message %s; timing details: %s' % (e, timing_message)) # Using the remote webservice failed, but maybe the local API will # be more successful? If this succeeds, then we should not indicate # a failure on the status light / dashboard, but we should still log # the remote failure Log(web_publish_failure_msg) local_publish_error_msg = PublishMessageLocal( s, timeout=timeout, update_dashboard=False) if not local_publish_error_msg: status_msg = ( 'Local service published (1) because web service failed with %s' % web_publish_failure_msg) publish_status_code = 'WARNING' else: status_msg = ( 'Local service failed (2) with %s after web service failed with %s' % (local_publish_error_msg, web_publish_failure_msg)) publish_status_code = 'FAILURE' else: # you may want to check HTTP response code, e.g. timing_message = CurlTimingDetailsToString(curl) status_code = curl.getinfo(pycurl.RESPONSE_CODE) if status_code != 200: web_publish_failure_msg = ( 'Server returned HTTP status code %d for message %s; ' 'timing details: %s' % (status_code, s, timing_message)) Log(web_publish_failure_msg) local_publish_error_msg = PublishMessageLocal( s, timeout=timeout, update_dashboard=False) if not local_publish_error_msg: status_msg = ( 'Local service published (3) because web service failed with %s' % web_publish_failure_msg) publish_status_code = 'WARNING' else: status_msg = ( 'Local service failed (4) with %s after web service failed with %s' % (local_publish_error_msg, web_publish_failure_msg)) publish_status_code = 'FAILURE' # We've logged the error code from the external web service, but the # Vestaboard local API was able to recover from the error, so we need not # log the failure message / error code in the dashboard. if not error_code: web_publish_failure_msg = '' curl.close() UpdateStatusLight( GPIO_ERROR_VESTABOARD_CONNECTION, publish_status_code=='FAILURE', web_publish_failure_msg) return status_msg, publish_status_code def PublishMessageLocal( s, local_key=LOCAL_KEY, local_address=LOCAL_VESTABOARD_ADDRESS, timeout=5, update_dashboard=True): """Publishes a text string to a Vestaboard via local API. The message is pushed to the vestaboard splitflap display by way of its local API; see https://docs.vestaboard.com/local for more details. 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 = {} timing['total-time'] = curl.getinfo(pycurl.TOTAL_TIME) timing['namelookup-time'] = curl.getinfo(pycurl.NAMELOOKUP_TIME) timing['connect-time'] = curl.getinfo(pycurl.CONNECT_TIME) timing['pretransfer-time'] = curl.getinfo(pycurl.PRETRANSFER_TIME) timing['redirect-time'] = curl.getinfo(pycurl.REDIRECT_TIME) timing['starttransfer-time'] = curl.getinfo(pycurl.STARTTRANSFER_TIME) results = [label + '=' + '%.4f' % timing[label] for label in timing] results = '; '.join(results) return results def TruncateEscapedLine(s): """Formats a single line of the personal message for the Vestaboard. The Vestaboard has line length limitations, a limited character set, <----SKIPPED LINES----> # 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): publish_status_msg = ( 'Message purged as no longer meets display criteria') publish_status_code = 'WARNING' 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) publish_status = PublishMessageWeb(splitflap_message) publish_status_msg, publish_status_code = publish_status if message_type in (FLAG_MSG_INSIGHT, FLAG_MSG_FLIGHT): flight = message[2] # We record flight number, time stamp, message, and status # to a pickle file so that we may construct a flight-centric # status report # # Specifically, we record a 3-tuple: # - flight number <----SKIPPED LINES----> ResetLogs(configuration) # clear the logs if requested UpdateRollingLogSize(configuration) # if this is a SIMULATION, then process every diff dump. But if it # isn't a simulation, then only read & do related processing for the # next dump if the last-modified timestamp indicates the file has been # updated since it was last read. tmp_timestamp = 0 if not SIMULATION: dump_json_exists = os.path.exists(DUMP_JSON_FILE) if dump_json_exists: tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE) if (SIMULATION and DumpJsonChanges()) or ( not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp): last_dump_json_timestamp = tmp_timestamp (persistent_nearby_aircraft, flight, now, json_desc_dict, persistent_path, flight_aware_error_message, flight_aware_timestamp) = ( ScanForNewFlights( persistent_nearby_aircraft, persistent_path, configuration.get('log_jsons', False), flights)) # Logging: As part of the memory instrumentation, let's track # the length of these data structures if not iteration % 1000: lengths = [len(flights), len(screen_history)] Log('Iteration: %d: object lengths: %s' % (iteration, lengths)) # because this might just be an updated instance of the previous # flight as more identifier information (squawk and or flight number) # comes in, we only want to process this if its a truly new flight new_flight_flag = ConfirmNewFlight(flight, flights) if new_flight_flag: # now comes directly from the radio json time_new_flight_found = now <----SKIPPED LINES----> flight, PICKLE_FLIGHTS, True, timestamp=flight['now']) # We record flight number, flight_meets_display_criteria, # reason_flight_fails_criteria, flight_aware_error_message, and # time stamps of when we confirmed new flight; when we generated # message; when we generated insight messages (if any), to a # to a pickle file so that we may construct a flight-centric # status report # # Specifically, we record a 3-tuple: # - flight number # - time stamp of the recording # - a dictionary of elements data = ( DisplayFlightNumber(flight), time.time(), { 'reason_flight_fails_criteria': reason_flight_fails_criteria, 'flight_aware_timestamp': flight_aware_timestamp, 'flight_aware_error_message': flight_aware_error_message, 'time_new_flight_found': time_new_flight_found, 'time_flight_message_inserted': time_flight_message_inserted, 'time_insight_message_inserted': time_insight_message_inserted}) PickleObjectToFile(data, PICKLE_FLIGHT_DASHBOARD, True) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration, screen_history) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) personal_message = PersonalMessage( configuration, message_queue, personal_message) # MEMORY MANAGEMENT # it turns out we only need the last screen in the screen history, not # the entire history, so we can purge all the rest from active memory <----SKIPPED LINES----> |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
1617181920212223242526272829303132333435363738394041424344454647484950515253545556 157158159160161162163164165166167168169170171172173174175176 177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 280281282283284285286287288289290291292293294295296297298299 300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 1525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567 15771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656 19211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012 57075708570957105711571257135714571557165717571857195720572157225723572457255726 57275728572957305731573257335734573557365737573857395740574157425743574457455746 64006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443 64456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584 67116712671367146715671667176718671967206721672267236724672567266727672867296730 67316732673367346735673667376738673967406741674267436744674567466747674867496750 732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364 74257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465 | <----SKIPPED LINES----> import statistics import sys import textwrap import time import tracemalloc import types import bs4 import dateutils import numpy import matplotlib import matplotlib.pyplot import psutil import pycurl import pytz import requests import tzlocal import unidecode # pylint: disable=line-too-long from constants import RASPBERRY_PI, MESSAGEBOARD_PATH, WEBSERVER_PATH, SECURE_WEBSERVER_PATH, KEY, SECRET, SUBSCRIPTION_ID, LOCAL_KEY, LOCAL_VESTABOARD_ADDRESS # pylint: enable=line-too-long import arduino if RASPBERRY_PI: import gpiozero # pylint: disable=E0401 import RPi.GPIO # pylint: disable=E0401 VERBOSE = False # additional messages logged SHUTDOWN_SIGNAL = '' REBOOT_SIGNAL = False # to be tracked in the dashboard messages so that we know when a # restart due to exit (vs. a long delay in some processing) happened INSTANCE_START_TIME = time.time() SIMULATION = False SIMULATION_COUNTER = 0 <----SKIPPED LINES----> # always leveraged by the running software. CONFIG_FILE = 'secure/settings.txt' CONFIG_BOOLEANS = ( 'setting_screen_enabled', 'next_flight', 'reset_logs', 'log_jsons') # A few key settings for the messageboard are its sensitivity to displaying # flights - though it logs all flights within range, it may not be desirable # to display all flights to the user. Two key parameters are the maximum # altitude, and the furthest away we anticipate the flight being at its # closest point to HOME. As those two parameters are manipulated in the # settings, a histogram is displayed with one or potentially two series, # showing the present and potentially prior-set distribution of flights, # by hour throughout the day, over the last seven days, normalized to # flights per day. This allows those parameters to be fine-tuned in a # useful way. This file is the location, on the webserver, of that image, # which needs to be in alignment with the html page that displays it. HOURLY_IMAGE_FILE = 'hours.png' # This is all messages that have been sent to the board since the last time # the file was manually cleared. Newest messages are at the bottom. It is # visible at the webserver. # This shows the most recent n messages sent to the board. Newest messages # are at the top for easier viewing of "what did I miss". ROLLING_MESSAGE_FILE = 'rolling_messages.txt' STDERR_FILE = 'secure/stderr.txt' BACKUP_FILE = 'secure/backup.txt' SERVICE_VERIFICATION_FILE = 'secure/service-verification.txt' UPTIMES_FILE = 'uptimes.php' CODE_HISTORY_FILE = 'code_history.php' FLIGHT_STATUS = 'flight_status.php' # This keeps a log of all aircraft not yet cataloged in the AIRCRAFT_LENGTH dict NEW_AIRCRAFT_FILE = 'secure/new_aircraft.txt' FLAG_MSG_FLIGHT = 1 # basic flight details FLAG_MSG_INSIGHT = 2 # random tidbit about a flight FLAG_MSG_HISTOGRAM = 3 # histogram message FLAG_MSG_CLEAR = 4 # a blank message to clear the screen # user-entered message to display for some duration of time FLAG_MSG_PERSONAL = 5 FLAG_INSIGHT_LAST_SEEN = 0 FLAG_INSIGHT_DIFF_AIRCRAFT = 1 FLAG_INSIGHT_NTH_FLIGHT = 2 FLAG_INSIGHT_GROUNDSPEED = 3 FLAG_INSIGHT_ALTITUDE = 4 FLAG_INSIGHT_VERTRATE = 5 FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 <----SKIPPED LINES----> ' function gtag(){dataLayer.push(arguments);}\n' " gtag('js', new Date());\n" " gtag('config', 'UA-99931533-2');\n" '</script>\n') #if running on raspberry, then need to prepend path to file names if RASPBERRY_PI: MEMORY_DIRECTORY = MESSAGEBOARD_PATH + MEMORY_DIRECTORY PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_SYSTEM_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_SYSTEM_DASHBOARD PICKLE_FLIGHT_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_FLIGHT_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 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 FLIGHT_HISTORY = SECURE_WEBSERVER_PATH + UPTIMES_FILE CODE_HISTORY_FILE = WEBSERVER_PATH + CODE_HISTORY_FILE NEW_AIRCRAFT_FILE = WEBSERVER_PATH + NEW_AIRCRAFT_FILE FLIGHT_STATUS = SECURE_WEBSERVER_PATH + FLIGHT_STATUS 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----> persistent_path: dictionary where keys are flight numbers, and the values are a sequential list of the location-attributes in the json file; allows for tracking the flight path over time. log_jsons: boolean indicating whether we should pickle the JSONs. flights: list of flight dictionaries; if no json is returned, used to find a recent flight with same flight number to augment this flight with origin / destination / airline. Returns: A tuple: - updated persistent_nearby_aircraft - (possibly empty) dictionary of flight attributes of the new flight upon its first observation. - the time of the radio observation if present; None if no radio dump - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest observed flight, etc.) - persistent_path, a data structure containing past details of a flight's location as described in ParseDumpJson - a text message indicating any errors in querying FlightAware or populating flight details - text string of SUCCESS, WARNING, or FAILURE: warning meaning the query occurred to soon after the last FA request, FAILURE if the request failed for some other reason, and SUCCESS if it was otherwise successful. - timestamp indicating exact time at which FlightAware was queried (or attempted to be queried), if a query was made in this pass """ flight_details = {} error_message = '' now = time.time() if SIMULATION: (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER] else: dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True) # Often there is no flight aware query, so we need to give a default value flight_aware_timestamp = 0 json_desc_dict = {} current_nearby_aircraft = {} if dump_json: (current_nearby_aircraft, now, json_desc_dict, persistent_path) = ParseDumpJson( dump_json, persistent_path) <----SKIPPED LINES----> # newly_nearby_flight_identifiers is a list of 2-tuples, where each # tuple is (flight_number, squawk) newly_nearby_flight_identifiers_str = ', '.join( ['%s/%s ' % (*i,) for i in newly_nearby_flight_identifiers]) newly_nearby_flight_details_str = '\n'.join([ str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers]) Log('Multiple newly-nearby flights: %s\n%s' % ( newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str)) flight_identifier = newly_nearby_flight_identifiers[0] flight_aware_json = {} if SIMULATION: json_times = [j[1] for j in FA_JSONS] if json_time in json_times: flight_aware_json = FA_JSONS[json_times.index(json_time)][0] flight_aware_timestamp = time.time() # "real" timestamp unavailable elif flight_identifier[0]: flight_number = flight_identifier[0] flight_aware_json, error_message, status, flight_aware_timestamp = ( GetFlightAwareJson(flight_number)) if flight_aware_json: UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False) else: failure_message = 'No json from Flightaware for flight %s: %s' % ( flight_number, error_message[:500]) Log(failure_message) UpdateStatusLight( GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True, failure_message) flight_details = {} if flight_aware_json: flight_details = ParseFlightAwareJson(flight_aware_json) elif flight_identifier[0]: # if there's a flight number but no json flight_details, derived_attr_msg = FindAttributesFromSimilarFlights( flight_identifier[0], flights) error_message = '%s; %s' % (error_message, derived_attr_msg) if not SIMULATION and log_jsons: PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True) # Augment FlightAware details with radio / radio-derived details flight_details.update(current_nearby_aircraft[flight_identifier]) # Augment with the past location data; the [1] is because recall that # persistent_path[key] is actually a 2-tuple, the first element being # the most recent time seen, and the second element being the actual # path. But we do not need to keep around the most recent time seen any # more. flight_details['persistent_path'] = persistent_path[flight_identifier][1] return ( persistent_nearby_aircraft, flight_details, now, json_desc_dict, persistent_path, error_message, status, flight_aware_timestamp) def DescribeDumpJson(parsed): """Generates dict with descriptive attributes about the dump json file. Args: parsed: The parsed json file. Returns: Dictionary with attributes about radio range, number of flights seen, etc. """ json_desc_dict = {} json_desc_dict['now'] = parsed['now'] aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS] json_desc_dict['radio_range_flights'] = len(aircraft) aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a] current_distances = [HaversineDistanceMeters( <----SKIPPED LINES----> (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: Four tuple: - Text representation of the json message from FlightAware. - Text string of error message, if any - Text string of SUCCESS, WARNING, or FAILURE: warning meaning the query occurred to soon after the last FA request, FAILURE if the request failed for some other reason, and SUCCESS if it was otherwise successful. - Timestamp of attempted query on FlightAware """ 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 %s at %s since last query to FA was only' ' %d seconds ago; min of %d seconds needed: %s' % ( flight_number, EpochDisplayTime(time.time(), format_string='%H:%M:%S'), seconds_since_last_query, min_query_delay_seconds, url)) flight_aware_status_code = 'WARNING' return '', error_msg, flight_aware_status_code, time.time() last_query_time = time.time() try: response = requests.get(url, timeout=5) query_time = time.time() except requests.exceptions.RequestException as e: query_time = time.time() # did not get to the query_time assignment above error_msg = 'Unable to query FA for URL due to %s: %s' % (e, url) flight_aware_status_code = 'FAILURE' return '', error_msg, flight_aware_status_code, query_time 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) flight_aware_status_code = 'FAILURE' return '', error_msg, flight_aware_status_code, query_time 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] flight_aware_status_code = 'SUCCESS' return flight_json, '', flight_aware_status_code, query_time def Unidecode(s): """Convert a special unicode characters to closest ASCII representation.""" if s is not None: s = unidecode.unidecode(s) return s def FindAttributesFromSimilarFlights(this_flight_number, flights): """Returns a dictionary with info about a flight based on other flights. We may not get a json from the internet about this flight for any number of reasons: internet down; website down; too frequent queries; etc. However, there are still some basic attributes we can derive about this flight from past observations of the same flight number, or past observations about the flight number prefix. Specifically, we can get the flight's airline, origin, and destination. Args: <----SKIPPED LINES----> f.close() def SimulationSetup(): """Updates global variable file names and loads JSON data for simulations.""" # Clear file so that shell tail -f process can continue to point to same file def ClearFile(filename): if os.path.exists(filename): with open(filename, 'w') as f: f.write('') global SIMULATION SIMULATION = True global DUMP_JSONS DUMP_JSONS = UnpickleObjectFromFile(PICKLE_DUMP_JSON_FILE, True) global FA_JSONS FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True) global LOGFILE LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX) ClearFile(LOGFILE) global ROLLING_LOGFILE ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX) ClearFile(ROLLING_LOGFILE) global ROLLING_MESSAGE_FILE ROLLING_MESSAGE_FILE = PrependFileName( ROLLING_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ROLLING_MESSAGE_FILE) global PICKLE_FLIGHTS PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX) filenames = UnpickleObjectFromFile( PICKLE_FLIGHTS, True, max_days=None, filenames=True) for file in filenames: ClearFile(file) <----SKIPPED LINES----> The message is pushed to the vestaboard splitflap display by way of its web services; see https://docs.vestaboard.com/introduction for more details; if the web service fails, it then reattempts to publish using the local api. TODO: rewrite to use the easier-to-follow requests library, more in line with PublishMessageLocal. Args: s: String to publish. subscription_id: string subscription id from Vestaboard. key: string key from Vestaboard. secret: string secret from Vestaboard. timeout: Max duration in seconds that we should wait to establish a connection. Returns: Two-tuple: - Text string indicating how the message was displayed (web or local api), including error messages encountered if any. - Status code which is one of the text strings SUCCESS, WARNING, or FAILURE, indicating whether the web service was used (SUCCESS), the local service was used because the web service failed (WARNING), or both failed (FAILURE). """ error_code = False curl = pycurl.Curl() # See https://stackoverflow.com/questions/31826814/ # curl-post-request-into-pycurl-code # Set URL value curl.setopt( pycurl.URL, 'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id) curl.setopt(pycurl.HTTPHEADER, [ 'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret]) curl.setopt(pycurl.TIMEOUT_MS, timeout*1000) curl.setopt(pycurl.POST, 1) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean # preparing body the way pycurl.READDATA wants it body_as_dict = {'characters': StringToCharArray(s)} <----SKIPPED LINES----> body_as_file_object = io.StringIO(body_as_json_string) # prepare and send. See also: pycurl.READFUNCTION to pass function instead curl.setopt(pycurl.READDATA, body_as_file_object) curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string)) web_publish_failure_msg = '' try: curl.perform() status_msg = 'Web service published' publish_status_code = 'SUCCESS' except pycurl.error as e: timing_message = CurlTimingDetailsToString(curl) web_publish_failure_msg = ( 'curl.perform() failed with message %s; timing details: %s' % (e, timing_message)) # Using the remote webservice failed, but maybe the local API will # be more successful? If this succeeds, then we should not indicate # a failure on the status light / dashboard, but we should still log # the remote failure Log(web_publish_failure_msg) local_publish_error_code, local_publish_error_msg = PublishMessageLocal( s, timeout=timeout, update_dashboard=False) if not local_publish_error_code: status_msg = ( '(1) Web service failed with %s so local service used' % web_publish_failure_msg) publish_status_code = 'WARNING' else: status_msg = ( '(2) Web service failed with %s; then local service failed with %s' % (web_publish_failure_msg, local_publish_error_msg)) publish_status_code = 'FAILURE' else: # you may want to check HTTP response code, e.g. timing_message = CurlTimingDetailsToString(curl) status_code = curl.getinfo(pycurl.RESPONSE_CODE) if status_code != 200: web_publish_failure_msg = ( 'Server returned HTTP status code %d for message %s; ' 'timing details: %s' % (status_code, s, timing_message)) Log(web_publish_failure_msg) local_publish_error_code, local_publish_error_msg = PublishMessageLocal( s, timeout=timeout, update_dashboard=False) if not local_publish_error_code: status_msg = ( '(3) Web service failed with %s so local service used' % web_publish_failure_msg) publish_status_code = 'WARNING' else: status_msg = ( '(4) Web service failed with %s; ' 'then local service failed with %s' % (web_publish_failure_msg, local_publish_error_msg)) publish_status_code = 'FAILURE' # We've logged the error code from the external web service, but the # Vestaboard local API was able to recover from the error, so we need not # log the failure message / error code in the dashboard. if not error_code: web_publish_failure_msg = '' curl.close() UpdateStatusLight( GPIO_ERROR_VESTABOARD_CONNECTION, publish_status_code=='FAILURE', web_publish_failure_msg) return status_msg, publish_status_code def PublishMessageLocal( s, local_key=LOCAL_KEY, local_address=LOCAL_VESTABOARD_ADDRESS, timeout=5, update_dashboard=True): """Publishes a text string to a Vestaboard via local API. The message is pushed to the vestaboard splitflap display by way of its local API; see https://docs.vestaboard.com/local for more details. 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: 2-tuple: - error_code: False if successful; error message string if failure occurs. - status_msg: Text description of status. """ error_code = False error_msg = '' 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: status_msg = 'Unable to reach %s (%s): %s' % (local_address, response, e) Log(status_msg) error_code = True if not error_code: status_msg = 'Message sent to Vestaboard by way of local api: %s' % s Log(status_msg) if update_dashboard: UpdateStatusLight( GPIO_ERROR_VESTABOARD_CONNECTION, error_code, error_msg) return error_code, status_msg def CurlTimingDetailsToString(curl): """Extracts timing details of a curl request into a readable string.""" timing = {} timing['total-time'] = curl.getinfo(pycurl.TOTAL_TIME) timing['namelookup-time'] = curl.getinfo(pycurl.NAMELOOKUP_TIME) timing['connect-time'] = curl.getinfo(pycurl.CONNECT_TIME) timing['pretransfer-time'] = curl.getinfo(pycurl.PRETRANSFER_TIME) timing['redirect-time'] = curl.getinfo(pycurl.REDIRECT_TIME) timing['starttransfer-time'] = curl.getinfo(pycurl.STARTTRANSFER_TIME) results = [label + '=' + '%.4f' % timing[label] for label in timing] results = '; '.join(results) return results def TruncateEscapedLine(s): """Formats a single line of the personal message for the Vestaboard. The Vestaboard has line length limitations, a limited character set, <----SKIPPED LINES----> # 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): publish_status_msg = ( 'Message purged as no longer meets display criteria') publish_status_code = 'WARNING' 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) # 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) publish_status = PublishMessageWeb(splitflap_message) publish_status_msg, publish_status_code = publish_status if message_type in (FLAG_MSG_INSIGHT, FLAG_MSG_FLIGHT): flight = message[2] # We record flight number, time stamp, message, and status # to a pickle file so that we may construct a flight-centric # status report # # Specifically, we record a 3-tuple: # - flight number <----SKIPPED LINES----> ResetLogs(configuration) # clear the logs if requested UpdateRollingLogSize(configuration) # if this is a SIMULATION, then process every diff dump. But if it # isn't a simulation, then only read & do related processing for the # next dump if the last-modified timestamp indicates the file has been # updated since it was last read. tmp_timestamp = 0 if not SIMULATION: dump_json_exists = os.path.exists(DUMP_JSON_FILE) if dump_json_exists: tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE) if (SIMULATION and DumpJsonChanges()) or ( not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp): last_dump_json_timestamp = tmp_timestamp (persistent_nearby_aircraft, flight, now, json_desc_dict, persistent_path, flight_aware_error_message, flight_aware_status_code, flight_aware_timestamp) = ( ScanForNewFlights( persistent_nearby_aircraft, persistent_path, configuration.get('log_jsons', False), flights)) # Logging: As part of the memory instrumentation, let's track # the length of these data structures if not iteration % 1000: lengths = [len(flights), len(screen_history)] Log('Iteration: %d: object lengths: %s' % (iteration, lengths)) # because this might just be an updated instance of the previous # flight as more identifier information (squawk and or flight number) # comes in, we only want to process this if its a truly new flight new_flight_flag = ConfirmNewFlight(flight, flights) if new_flight_flag: # now comes directly from the radio json time_new_flight_found = now <----SKIPPED LINES----> flight, PICKLE_FLIGHTS, True, timestamp=flight['now']) # We record flight number, flight_meets_display_criteria, # reason_flight_fails_criteria, flight_aware_error_message, and # time stamps of when we confirmed new flight; when we generated # message; when we generated insight messages (if any), to a # to a pickle file so that we may construct a flight-centric # status report # # Specifically, we record a 3-tuple: # - flight number # - time stamp of the recording # - a dictionary of elements data = ( DisplayFlightNumber(flight), time.time(), { 'reason_flight_fails_criteria': reason_flight_fails_criteria, 'flight_aware_timestamp': flight_aware_timestamp, 'flight_aware_error_message': flight_aware_error_message, 'flight_aware_status_code': flight_aware_status_code, 'time_new_flight_found': time_new_flight_found, 'time_flight_message_inserted': time_flight_message_inserted, 'time_insight_message_inserted': time_insight_message_inserted}) PickleObjectToFile(data, PICKLE_FLIGHT_DASHBOARD, True) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration, screen_history) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) personal_message = PersonalMessage( configuration, message_queue, personal_message) # MEMORY MANAGEMENT # it turns out we only need the last screen in the screen history, not # the entire history, so we can purge all the rest from active memory <----SKIPPED LINES----> |