messageboard-2022-10-02-1507.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









9596979899100101102103104105106107108109110111112113114115116117118      119120121122123124125126127128129130131132133134135136137138








268269270271272273274275276277278279280281282283284285286287288 289290291292293294295296297298299300301302303304305306307308








15211522152315241525152615271528152915301531153215331534153515361537153815391540  15411542 15431544154515461547154815491550155115521553155415551556155715581559156015611562








1578157915801581158215831584158515861587158815891590159115921593159415951596159715981599 16001601160216031604160516061607160816091610161116121613161416151616161716181619 16201621162216231624162516261627162816291630163116321633163416351636163716381639








19891990199119921993199419951996199719981999200020012002200320042005200620072008 2009 20102011 20122013201420152016201720182019202020212022202320242025202620272028202920302031








20382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060 206120622063206420652066206720682069207020712072207320742075207620772078207920802081








248524862487248824892490249124922493249424952496249724982499250025012502250325042505 250625072508 2509251025112512    25132514251525162517251825192520252125222523    252425252526252725282529253025312532253325342535    25362537253825392540254125422543    25442545254625472548254925502551  25522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577








26042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644








31713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211








5718571957205721572257235724572557265727572857295730573157325733573457355736573757385739 57405741574257435744       57455746574757485749575057515752575357545755575657575758575957605761576257635764








63776378637963806381638263836384638563866387638863896390639163926393639463956396    63976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425 64266427642864296430643164326433643464356436        64376438643964406441644264436444644564466447        6448644964506451645264536454645564566457  64586459646064616462646364646465646664676468646964706471647264736474647564766477








66156616661766186619662066216622662366246625662666276628662966306631663266336634 663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658 6659666066616662666366646665666666676668666966706671667266736674667566766677                    66786679668066816682668366846685668666876688668966906691669266936694669566966697








68436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883








72247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264 726572667267726872697270 7271        7272727372747275727672777278 7279728072817282728372847285728672877288728972907291729272937294729572967297729872997300 73017302730373047305730673077308                      73097310731173127313731473157316731773187319732073217322732373247325732673277328











                            <----SKIPPED LINES---->





# 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_'

# This web-exposed file is used for non-error messages that might highlight
# data or code logic to check into. It is only cleared out manually.
LOGFILE = 'log.txt'
# Identical to the LOGFILE, except it includes just the most recent n lines.
# Newest lines are at the end.
ROLLING_LOGFILE = 'secure/rolling_log.txt' #file for error messages

# default number of lines which may be overridden by settings file
ROLLING_LOG_SIZE = 1000

# Users can trigger .png histograms analogous to the text ones from the web
# interface; this is the folder (within WEBSERVER_PATH) where those files are
# placed
WEBSERVER_IMAGE_RELATIVE_FOLDER = 'images/'
# Multiple histograms can be generated, i.e. for airline, aircraft, day of
# week, etc. The output files are named by the prefix & suffix, i.e.: prefix +
# type + . + suffix, as in histogram_aircraft.png. These names match up to the




                            <----SKIPPED LINES---->




    8, 'Unused', False)

# GPIO pushbutton connections - (GPIO pin switch in; GPIO pin LED out)
GPIO_SOFT_RESET = (20, 21)

GOOGLE_ANALYTICS_TAG = (
    '<!-- Global site tag (gtag.js) - Google Analytics -->\n'
    '<script async src="https://www.googletagmanager.com/gtag/'
    'js?id=UA-99931533-2"></script>\n'
    '<script>\n'
    '  window.dataLayer = window.dataLayer || [];\n'
    '  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_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




                            <----SKIPPED LINES---->




    persistent_nearby_aircraft: dictionary where keys are flight numbers, and
      the values are the time the flight was last seen.
    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


  """
  flight_details = {}

  now = time.time()
  if SIMULATION:
    (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER]
  else:
    dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True)

  json_desc_dict = {}
  current_nearby_aircraft = {}
  if dump_json:
    (current_nearby_aircraft, now,
     json_desc_dict, persistent_path) = ParseDumpJson(
         dump_json, persistent_path)

    if not SIMULATION and log_jsons:
      PickleObjectToFile((dump_json, now), PICKLE_DUMP_JSON_FILE, True)

    newly_nearby_flight_identifiers = UpdateAircraftList(
        persistent_nearby_aircraft, current_nearby_aircraft, now)

    if newly_nearby_flight_identifiers:




                            <----SKIPPED LINES---->




      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]
      elif flight_identifier[0]:
        flight_number = flight_identifier[0]
        flight_aware_json, error_message = 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 = FindAttributesFromSimilarFlights(
            flight_identifier[0], flights)


      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)



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(
      HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos]




                            <----SKIPPED LINES---->




  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:
    this_flight_number: String of this flight number.
    flights: List of past flights.

  Returns:

    Dictionary of flight attributes extracted from the FlightAware json.

  """
  derived_attributes = {}

  if not this_flight_number:
    return derived_attributes

  potential_values = {
      'origin_friendly': [],
      'origin_iata': [],
      'destination_friendly': [],
      'destination_iata': [],
      'airline_call_sign': [],
      'airline_short_name': [],
      'airline_full_name': []
      }

  def AppendIfPresent(k, flight):
    val = flight.get(k)
    if val and val != KEY_NOT_PRESENT_STRING:
      potential_values[k].append(val)

  for flight in flights:
    flight_number = flight.get('flight_number')




                            <----SKIPPED LINES---->




      if flight_number[:3] == this_flight_number[:3]:
        AppendIfPresent('airline_call_sign', flight)
        AppendIfPresent('airline_short_name', flight)
        AppendIfPresent('airline_full_name', flight)

  def Mode(lst):
    freq = {}
    for val in lst:
      freq[val] = freq.get(val, 0) + 1
    max_freq = max(freq.values())
    for val in freq:
      if freq[val] == max_freq:
        return val
    return None

  for attribute, lst in potential_values.items():
    if lst:
      derived_attributes[attribute] = Mode(lst)

  if derived_attributes:
    Log(
        'Derived attributes for %s based on past flights: %s' %
        (this_flight_number, str(derived_attributes)))

  return derived_attributes


def ParseFlightAwareJson(flight_json):
  """Strips relevant data about the flight from FlightAware feed.

  The FlightAware json has hundreds of fields about a flight, only a fraction
  of which are relevant to extract. Note that some of the fields are
  inconsistently populated (i.e.: scheduled and actual times for departure and
  take-off).

  Args:
    flight_json: Text representation of the FlightAware json about a single
      flight.

  Returns:
    Dictionary of flight attributes extracted from the FlightAware json.
  """
  flight = {}
  parsed_json = json.loads(flight_json)





                            <----SKIPPED LINES---->




    flight, configuration, display_all_hours=False, log=False):
  """Returns boolean indicating whether the screen accepting new flight data.

  Based on the configuration file, determines whether the flight data should be
  displayed. Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of
    operation
  - should include altitude & elevation parameters indicating max values of
    interest

  Args:
    flight: dictionary of flight attributes.
    configuration: dictionary of configuration attributes.
    display_all_hours: a boolean indicating whether we should ignore whether the
      screen is turned off (either via the enabling, or via the hour settings)
    log: optional boolean indicating whether a flight that fails the criteria
      should be logged with the reason

  Returns:
    Boolean as described.

  """
  flight_altitude = flight.get('altitude', float('inf'))
  config_max_altitude = configuration['setting_max_altitude']


  flight_meets_criteria = True
  if flight_altitude > config_max_altitude:
    flight_meets_criteria = False




    if log:
      Log(
          '%s not displayed because it fails altitude criteria - '
          'flight altitude: %.0f; required altitude: %.0f' % (
              DisplayFlightNumber(flight),
              flight_altitude, config_max_altitude))
  else:
    flight_distance = flight.get('min_feet', float('inf'))
    config_max_distance = configuration['setting_max_distance']
    if flight_distance > config_max_distance:
      flight_meets_criteria = False




      if log:
        Log(
            '%s not displayed because it fails distance criteria - '
            'flight distance: %.0f; required distance: %.0f' % (
                DisplayFlightNumber(flight), flight_distance,
                config_max_distance))

  if not display_all_hours and flight_meets_criteria:
    flight_timestamp = flight['now']
    minute_of_day = MinuteOfDay(flight_timestamp)
    if minute_of_day < configuration['setting_on_time']:
      flight_meets_criteria = False




      if log:
        Log(
            '%s not displayed because it occurs too early - minute_of_day: '
            '%d; setting_on_time: %d' % (
                DisplayFlightNumber(flight), minute_of_day,
                configuration['setting_on_time']))
    elif minute_of_day > configuration['setting_off_time'] + 1:
      flight_meets_criteria = False




      if log:
        Log(
            '%s not displayed because it occurs too late - minute_of_day: '
            '%d; setting_off_time: %d' % (
                DisplayFlightNumber(flight), minute_of_day,
                configuration['setting_off_time']))
    elif configuration.get('setting_screen_enabled', 'off') == 'off':
      flight_meets_criteria = False


      if log:
        Log(
            '%s not displayed because screen disabled' %
            DisplayFlightNumber(flight))

  return flight_meets_criteria


def MessageMeetsDisplayCriteria(configuration, log=False):
  """Returns boolean indicating whether screen is active.

  Based on the configuration file, determines whether the flight data should be
  displayed. Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of
    operation
  Note that this differs from FlightMeetsDisplayCriteria in that this function
  does not use any attribute about the flight (elevation or distance), only
  whether the screen is still active.

  Args:
    configuration: dictionary of configuration attributes.
    log: optional boolean indicating whether a flight that fails the criteria
      should be logged with the reason

  Returns:




                            <----SKIPPED LINES---->






def IdentifyFlightDisplayed(flights, configuration, display_all_hours=False):
  """Finds the most recent flight in flights that meet the display criteria.

  Args:
    flights: list of flight dictionaries.
    configuration: dictionary of settings.
    display_all_hours: boolean indicating whether we should ignore the time
    constraints (i.e.: whether the screen is enabled, and its turn-on or
    turn-off times) in identifying the most recent flight. That is, if False,
    then this will only return flights that would have been displayed in the
    ordinarily usage, vs. if True, a flight irrespective of the time it would
    be displayed.

  Returns:
    A flight dictionary if one can be found; None otherwise.
  """
  for n in range(len(flights)-1, -1, -1):  # traverse the flights in reverse
    if FlightMeetsDisplayCriteria(
        flights[n], configuration, display_all_hours=display_all_hours):
      return n
  return None


def CreateMessageAboutFlight(flight):
  """Creates a message to describe interesting attributes about a single flight.

  Generates a multi-line description of a flight. A typical message might look
  like:
  UAL300 - UNITED        <- Flight number and airline
  BOEING 777-200 (TWIN)  <- Aircraft type
  SFO-HNL HONOLULU       <- Origin & destination
  DEP 02:08 ER REM 5:14  <- Time details: departure; early / late; remaining
  185MPH 301DEG D:117FT  <- Trajectory: speed; bearing; fcst min dist to HOME
  1975FT (+2368FPM)      <- Altitude: current altitude & rate of ascent

  However, not all of these details are always present, so some may be listed
  as unknown, or entire lines may be left out.

  Args:




                            <----SKIPPED LINES---->




  this_flight = flights[-1]
  this_hour = int(DisplayTime(this_flight, '%-H'))
  this_minute = int(DisplayTime(this_flight, '%-M'))
  this_date = DisplayTime(this_flight, '%x')

  # Flights that we've already seen in the last few hours we do not expect to
  # see again for another few hours, so let's exclude them from the calculation
  exclude_flights_hours = 12
  flight_numbers_seen_in_last_n_hours = [
      f['flight_number'] for f in flights
      if f['now'] > this_flight['now'] - exclude_flights_hours*SECONDS_IN_HOUR
      and 'flight_number' in f]
  still_to_come_flights = [
      f for f in flights[:-1]
      if f.get('flight_number') not in flight_numbers_seen_in_last_n_hours
      and this_date != DisplayTime(f, '%x')]

  # exclude flights that would be filtered out by altitude or distance
  still_to_come_flights = [
      f for f in still_to_come_flights
      if FlightMeetsDisplayCriteria(f, configuration)]

  # exclude flights more than 30 days in the past
  now = time.time()
  still_to_come_flights = [
      f for f in still_to_come_flights
      if now - f['now'] < MAX_INSIGHT_HORIZON_DAYS * SECONDS_IN_DAY]

  minimum_minutes_next_flight = {}  # min minutes to next flight by day
  for flight in still_to_come_flights:
    date = DisplayTime(flight, '%x')
    hour = int(DisplayTime(flight, '%-H'))
    minutes = int(DisplayTime(flight, '%-M'))
    minutes_after = (hour - this_hour) * MINUTES_IN_HOUR + (
        minutes - this_minute)
    if minutes_after < 0:
      minutes_after += MINUTES_IN_DAY
    minimum_minutes_next_flight[date] = min(
        minimum_minutes_next_flight.get(date, minutes_after), minutes_after)

  minutes = list(minimum_minutes_next_flight.values())




                            <----SKIPPED LINES---->




  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)

  global PICKLE_DASHBOARD
  PICKLE_DASHBOARD = PrependFileName(PICKLE_DASHBOARD, SIMULATION_PREFIX)

  filenames = UnpickleObjectFromFile(
      PICKLE_DASHBOARD, True, max_days=None, filenames=True)
  for file in filenames:
    ClearFile(file)









def SimulationEnd(message_queue, flights, screens):
  """Clears message buffer, exercises histograms, and other simulation wrap up.

  Args:
    message_queue: List of flight messages that have not yet been printed.
    flights: List of flights dictionaries.
    screens: List of past screens displayed to splitflap screen.
  """
  if flights:
    histogram = {
        'type': 'both',
        'histogram':'all',
        'histogram_history':'30d',
        'histogram_max_screens': '_2',
        'histogram_data_summary': 'on'}
    message_queue.extend(TriggerHistograms(flights, histogram))

    while message_queue:
      ManageMessageQueue(message_queue, 0, {'setting_delay': 0}, screens)




                            <----SKIPPED LINES---->




    s,
    subscription_id=SUBSCRIPTION_ID,
    key=KEY,
    secret=SECRET,
    timeout=5):
  """Publishes a text string to a Vestaboard.

  The message is pushed to the vestaboard splitflap display by way of its
  web services; see https://docs.vestaboard.com/introduction for more details.

  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.




  """
  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)}
  body_as_json_string = json.dumps(body_as_dict) # dict to json
  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))
  failure_message = ''
  try:
    curl.perform()

  except pycurl.error as e:
    timing_message = CurlTimingDetailsToString(curl)
    failure_message = (
        '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(failure_message)
    error_code = PublishMessageLocal(s, timeout=timeout, update_dashboard=False)








  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:
      failure_message = (
          'Server returned HTTP status code %d for message %s; '
          'timing details: %s' % (status_code, s, timing_message))
      Log(failure_message)
      error_code = PublishMessageLocal(
          s, timeout=timeout, update_dashboard=False)









  # 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:
    failure_message = ''

  curl.close()
  UpdateStatusLight(
      GPIO_ERROR_VESTABOARD_CONNECTION, error_code, failure_message)




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




                            <----SKIPPED LINES---->





  return personal_message


def ManageMessageQueue(
    message_queue, next_message_time, configuration, screens):
  """Check time & if appropriate, display next message from queue.

  Args:
    message_queue: FIFO list of message tuples of (message type,
      message string).
    next_message_time: epoch at which next message should be displayed
    configuration: dictionary of configuration attributes.
    screens: List of past screens displayed to splitflap screen.

  Returns:
    Next_message_time, potentially updated if a message has been displayed,
    or unchanged if no message was displayed.
  """
  if message_queue and (time.time() >= next_message_time or SIMULATION):


    if SIMULATION:  # drain the queue because the messages come so fast
      messages_to_display = list(message_queue)
      # passed by reference, so clear it out since we drained it to the display
      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']
  return next_message_time


def DeleteMessageTypes(q, types_to_delete):
  """Delete messages from the queue if type is in the iterable types."""
  if VERBOSE:
    messages_to_delete = [m for m in q if m[0] in types_to_delete]
    if messages_to_delete:
      Log('Deleting messages from queue due to new-found plane: %s'
          % messages_to_delete)
  updated_q = [m for m in q if m[0] not in types_to_delete]
  return updated_q


def BootstrapInsightList(full_path=PICKLE_FLIGHTS):
  """(Re)populate flight pickle files with flight insight distributions.

  The set of insights generated for each flight is created at the time




                            <----SKIPPED LINES---->




  Args:
    value: Boolean indicating whether a failure has occurred (True) or
      system is nominal (False).
    subsystem: A tuple describing the system; though that description may
      have multiple attributes, the 0th element is the numeric identifier
      of that system.  monitoring.py depends on other attributes of that
      tuple being present as well.  Since the overall system does not have
      a tuple defined for it, it gets a default identifier of 0.
    failure_message: an (optional) message describing why the system /
      subsystem is being disabled or failing.
    iteration: integer indicating how many times the main loop has been
      completed.
  """
  versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO)
  if subsystem:
    subsystem = subsystem[0]
  PickleObjectToFile((
      time.time(), subsystem, value, versions,
      failure_message, INSTANCE_START_TIME,
      iteration, gpiozero.CPUTemperature().temperature),
      PICKLE_DASHBOARD, True)


def RemoveFile(file):
  """Removes a file, returning a boolean indicating if it had existed."""
  if os.path.exists(file):

    try:
      os.remove(file)
    except PermissionError:
      return False
    return True

  return False


def ConfirmNewFlight(flight, flights):
  """Replaces last-seen flight with new flight if identifiers overlap.

  Flights are identified by the radio over time by a tuple of identifiers:
  flight_number and squawk.  Due to unknown communication issues, one or the




                            <----SKIPPED LINES---->




    configuration = new_configuration

    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) = 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:

        flights.append(flight)
        remote, servo = RefreshArduinos(
            remote, servo,
            to_remote_q, to_servo_q, to_main_q, shutdown,
            flights, json_desc_dict, configuration, screen_history)


        if FlightMeetsDisplayCriteria(flight, configuration, log=True):








          personal_message = None  # Any personal message displayed now cleared
          flight_message = (
              FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight), flight)

          # display the next message about this flight now!
          next_message_time = time.time()
          message_queue.insert(0, flight_message)

          # and delete any queued insight messages about other flights that have
          # not yet displayed, since a newer flight has taken precedence
          message_queue = DeleteMessageTypes(message_queue, (FLAG_MSG_INSIGHT,))

          # Though we also manage the message queue outside this conditional
          # as well, because it can take a half second to generate the flight
          # insights, this allows this message to start displaying on the
          # board immediately, so it's up there when it's most relevant
          next_message_time = ManageMessageQueue(
              message_queue, next_message_time, configuration, screen_history)

          insight_messages = CreateFlightInsights(
              flights,
              configuration.get('insights'),
              insight_message_distribution)
          if configuration.get('next_flight', 'off') == 'on':
            next_flight_text = FlightInsightNextFlight(flights, configuration)
            if next_flight_text:
              insight_messages.insert(0, next_flight_text)

          insight_messages = [(FLAG_MSG_INSIGHT, m) for m in insight_messages]


          for insight_message in insight_messages:
            message_queue.insert(0, insight_message)

        else:  # flight didn't meet display criteria
          flight['insight_types'] = []

        PickleObjectToFile(
            flight, PICKLE_FLIGHTS, True, timestamp=flight['now'])























      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
    # now that we've saved the flight, we have no more need to keep the
    # memory-hogging persistent_path key of that flight in live memory
    if 'persistent_path' in flights[-1]:
      del flights[-1]['persistent_path']
    # 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









9596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144








274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315








152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572








1588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651








2001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046








20532054205520562057205820592060206120622063206420652066206720682069207020712072 207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096








250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535    25362537253825392540254125422543254425452546    254725482549255025512552255325542555255625572558    25592560256125622563256425652566    256725682569257025712572  2573257425752576257725782579258025812582258325842585258625872588258925902591259225932594








26212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661








31883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228








5735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789








6402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525








666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767








69136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953








729472957296729772987299730073017302730373047305730673077308730973107311731273137314  73157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430











                            <----SKIPPED LINES---->





# 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 systems - are they 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_SYSTEM_DASHBOARD = 'pickle/dashboard.pk'

# Flight-centric status data - what time was the flight first detected as
# in range, does it pass the display criteria, was a json available for it and
# when, was the local or web api used to communicate with the vestaboard
# and how much later, etc.
PICKLE_FLIGHT_DASHBOARD = 'pickle/flight_status.pk'

CACHED_ELEMENT_PREFIX = 'cached_'

# This web-exposed file is used for non-error messages that might highlight
# data or code logic to check into. It is only cleared out manually.
LOGFILE = 'log.txt'
# Identical to the LOGFILE, except it includes just the most recent n lines.
# Newest lines are at the end.
ROLLING_LOGFILE = 'secure/rolling_log.txt' #file for error messages

# default number of lines which may be overridden by settings file
ROLLING_LOG_SIZE = 1000

# Users can trigger .png histograms analogous to the text ones from the web
# interface; this is the folder (within WEBSERVER_PATH) where those files are
# placed
WEBSERVER_IMAGE_RELATIVE_FOLDER = 'images/'
# Multiple histograms can be generated, i.e. for airline, aircraft, day of
# week, etc. The output files are named by the prefix & suffix, i.e.: prefix +
# type + . + suffix, as in histogram_aircraft.png. These names match up to the




                            <----SKIPPED LINES---->




    8, 'Unused', False)

# GPIO pushbutton connections - (GPIO pin switch in; GPIO pin LED out)
GPIO_SOFT_RESET = (20, 21)

GOOGLE_ANALYTICS_TAG = (
    '<!-- Global site tag (gtag.js) - Google Analytics -->\n'
    '<script async src="https://www.googletagmanager.com/gtag/'
    'js?id=UA-99931533-2"></script>\n'
    '<script>\n'
    '  window.dataLayer = window.dataLayer || [];\n'
    '  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
  FLIGHTAWARE_HISTORY_FILE = WEBSERVER_PATH + FLIGHTAWARE_HISTORY_FILE

  HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML




                            <----SKIPPED LINES---->




    persistent_nearby_aircraft: dictionary where keys are flight numbers, and
      the values are the time the flight was last seen.
    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
  """
  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)

  json_desc_dict = {}
  current_nearby_aircraft = {}
  if dump_json:
    (current_nearby_aircraft, now,
     json_desc_dict, persistent_path) = ParseDumpJson(
         dump_json, persistent_path)

    if not SIMULATION and log_jsons:
      PickleObjectToFile((dump_json, now), PICKLE_DUMP_JSON_FILE, True)

    newly_nearby_flight_identifiers = UpdateAircraftList(
        persistent_nearby_aircraft, current_nearby_aircraft, now)

    if newly_nearby_flight_identifiers:




                            <----SKIPPED LINES---->




      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]
      elif flight_identifier[0]:
        flight_number = flight_identifier[0]
        flight_aware_json, error_message = 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 += 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)


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(
      HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos]




                            <----SKIPPED LINES---->




  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:
    this_flight_number: String of this flight number.
    flights: List of past flights.

  Returns:
    2-tuple of:
    - Dictionary of flight attributes extracted from the FlightAware json.
    - Status message indicating what attributes derived
  """
  derived_attributes = {}
  status_message = ''
  if not this_flight_number:
    return derived_attributes

  potential_values = {
      'origin_friendly': [],
      'origin_iata': [],
      'destination_friendly': [],
      'destination_iata': [],
      'airline_call_sign': [],
      'airline_short_name': [],
      'airline_full_name': []
      }

  def AppendIfPresent(k, flight):
    val = flight.get(k)
    if val and val != KEY_NOT_PRESENT_STRING:
      potential_values[k].append(val)

  for flight in flights:
    flight_number = flight.get('flight_number')




                            <----SKIPPED LINES---->




      if flight_number[:3] == this_flight_number[:3]:
        AppendIfPresent('airline_call_sign', flight)
        AppendIfPresent('airline_short_name', flight)
        AppendIfPresent('airline_full_name', flight)

  def Mode(lst):
    freq = {}
    for val in lst:
      freq[val] = freq.get(val, 0) + 1
    max_freq = max(freq.values())
    for val in freq:
      if freq[val] == max_freq:
        return val
    return None

  for attribute, lst in potential_values.items():
    if lst:
      derived_attributes[attribute] = Mode(lst)

  if derived_attributes:

    status_message = 'Derived attributes for %s based on past flights: %s' % (
      this_flight_number, str(derived_attributes))
    Log(status_message)
  return derived_attributes, status_message


def ParseFlightAwareJson(flight_json):
  """Strips relevant data about the flight from FlightAware feed.

  The FlightAware json has hundreds of fields about a flight, only a fraction
  of which are relevant to extract. Note that some of the fields are
  inconsistently populated (i.e.: scheduled and actual times for departure and
  take-off).

  Args:
    flight_json: Text representation of the FlightAware json about a single
      flight.

  Returns:
    Dictionary of flight attributes extracted from the FlightAware json.
  """
  flight = {}
  parsed_json = json.loads(flight_json)





                            <----SKIPPED LINES---->




    flight, configuration, display_all_hours=False, log=False):
  """Returns boolean indicating whether the screen accepting new flight data.

  Based on the configuration file, determines whether the flight data should be
  displayed. Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of
    operation
  - should include altitude & elevation parameters indicating max values of
    interest

  Args:
    flight: dictionary of flight attributes.
    configuration: dictionary of configuration attributes.
    display_all_hours: a boolean indicating whether we should ignore whether the
      screen is turned off (either via the enabling, or via the hour settings)
    log: optional boolean indicating whether a flight that fails the criteria
      should be logged with the reason

  Returns:
    2-tuple: Boolean as described; text indication for why message not
    displayed.
  """
  flight_altitude = flight.get('altitude', float('inf'))
  config_max_altitude = configuration['setting_max_altitude']
  failure_message = ''

  flight_meets_criteria = True
  if flight_altitude > config_max_altitude:
    flight_meets_criteria = False
    failure_message = (
        '%s not displayed because it fails altitude criteria - '
        'flight altitude: %.0f; required altitude: %.0f' %
        (DisplayFlightNumber(flight), flight_altitude, config_max_altitude))
    if log:
      Log(failure_message)




  else:
    flight_distance = flight.get('min_feet', float('inf'))
    config_max_distance = configuration['setting_max_distance']
    if flight_distance > config_max_distance:
      flight_meets_criteria = False
      failure_message = (
          '%s not displayed because it fails distance criteria - '
          'flight distance: %.0f; required distance: %.0f' %
          (DisplayFlightNumber(flight), flight_distance, config_max_distance))
      if log:
        Log(failure_message)





  if not display_all_hours and flight_meets_criteria:
    flight_timestamp = flight['now']
    minute_of_day = MinuteOfDay(flight_timestamp)
    if minute_of_day < configuration['setting_on_time']:
      flight_meets_criteria = False
      failure_message = (
          '%s not displayed because it occurs too early - minute_of_day: '
          '%d; setting_on_time: %d' % (DisplayFlightNumber(flight),
          minute_of_day, configuration['setting_on_time']))
      if log:
        Log(failure_message)




    elif minute_of_day > configuration['setting_off_time'] + 1:
      flight_meets_criteria = False
      failure_message = (
        '%s not displayed because it occurs too late - minute_of_day: '
        '%d; setting_off_time: %d' % (DisplayFlightNumber(flight),
        minute_of_day, configuration['setting_off_time']))
      if log:
        Log(failure_message)




    elif configuration.get('setting_screen_enabled', 'off') == 'off':
      flight_meets_criteria = False
      failure_message = '%s not displayed because screen disabled' % (
          DisplayFlightNumber(flight))
      if log:
        Log(failure_message)



  return flight_meets_criteria, failure_message


def MessageMeetsDisplayCriteria(configuration, log=False):
  """Returns boolean indicating whether screen is active.

  Based on the configuration file, determines whether the flight data should be
  displayed. Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of
    operation
  Note that this differs from FlightMeetsDisplayCriteria in that this function
  does not use any attribute about the flight (elevation or distance), only
  whether the screen is still active.

  Args:
    configuration: dictionary of configuration attributes.
    log: optional boolean indicating whether a flight that fails the criteria
      should be logged with the reason

  Returns:




                            <----SKIPPED LINES---->






def IdentifyFlightDisplayed(flights, configuration, display_all_hours=False):
  """Finds the most recent flight in flights that meet the display criteria.

  Args:
    flights: list of flight dictionaries.
    configuration: dictionary of settings.
    display_all_hours: boolean indicating whether we should ignore the time
    constraints (i.e.: whether the screen is enabled, and its turn-on or
    turn-off times) in identifying the most recent flight. That is, if False,
    then this will only return flights that would have been displayed in the
    ordinarily usage, vs. if True, a flight irrespective of the time it would
    be displayed.

  Returns:
    A flight dictionary if one can be found; None otherwise.
  """
  for n in range(len(flights)-1, -1, -1):  # traverse the flights in reverse
    if FlightMeetsDisplayCriteria(
        flights[n], configuration, display_all_hours=display_all_hours)[0]:
      return n
  return None


def CreateMessageAboutFlight(flight):
  """Creates a message to describe interesting attributes about a single flight.

  Generates a multi-line description of a flight. A typical message might look
  like:
  UAL300 - UNITED        <- Flight number and airline
  BOEING 777-200 (TWIN)  <- Aircraft type
  SFO-HNL HONOLULU       <- Origin & destination
  DEP 02:08 ER REM 5:14  <- Time details: departure; early / late; remaining
  185MPH 301DEG D:117FT  <- Trajectory: speed; bearing; fcst min dist to HOME
  1975FT (+2368FPM)      <- Altitude: current altitude & rate of ascent

  However, not all of these details are always present, so some may be listed
  as unknown, or entire lines may be left out.

  Args:




                            <----SKIPPED LINES---->




  this_flight = flights[-1]
  this_hour = int(DisplayTime(this_flight, '%-H'))
  this_minute = int(DisplayTime(this_flight, '%-M'))
  this_date = DisplayTime(this_flight, '%x')

  # Flights that we've already seen in the last few hours we do not expect to
  # see again for another few hours, so let's exclude them from the calculation
  exclude_flights_hours = 12
  flight_numbers_seen_in_last_n_hours = [
      f['flight_number'] for f in flights
      if f['now'] > this_flight['now'] - exclude_flights_hours*SECONDS_IN_HOUR
      and 'flight_number' in f]
  still_to_come_flights = [
      f for f in flights[:-1]
      if f.get('flight_number') not in flight_numbers_seen_in_last_n_hours
      and this_date != DisplayTime(f, '%x')]

  # exclude flights that would be filtered out by altitude or distance
  still_to_come_flights = [
      f for f in still_to_come_flights
      if FlightMeetsDisplayCriteria(f, configuration)[0]]

  # exclude flights more than 30 days in the past
  now = time.time()
  still_to_come_flights = [
      f for f in still_to_come_flights
      if now - f['now'] < MAX_INSIGHT_HORIZON_DAYS * SECONDS_IN_DAY]

  minimum_minutes_next_flight = {}  # min minutes to next flight by day
  for flight in still_to_come_flights:
    date = DisplayTime(flight, '%x')
    hour = int(DisplayTime(flight, '%-H'))
    minutes = int(DisplayTime(flight, '%-M'))
    minutes_after = (hour - this_hour) * MINUTES_IN_HOUR + (
        minutes - this_minute)
    if minutes_after < 0:
      minutes_after += MINUTES_IN_DAY
    minimum_minutes_next_flight[date] = min(
        minimum_minutes_next_flight.get(date, minutes_after), minutes_after)

  minutes = list(minimum_minutes_next_flight.values())




                            <----SKIPPED LINES---->




  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)

  global PICKLE_SYSTEM_DASHBOARD
  PICKLE_SYSTEM_DASHBOARD = PrependFileName(
      PICKLE_SYSTEM_DASHBOARD, SIMULATION_PREFIX)
  filenames = UnpickleObjectFromFile(
      PICKLE_SYSTEM_DASHBOARD, True, max_days=None, filenames=True)
  for file in filenames:
    ClearFile(file)

  global PICKLE_FLIGHT_DASHBOARD
  PICKLE_FLIGHT_DASHBOARD = PrependFileName(
      PICKLE_FLIGHT_DASHBOARD, SIMULATION_PREFIX)
  filenames = UnpickleObjectFromFile(
      PICKLE_FLIGHT_DASHBOARD, True, max_days=None, filenames=True)
  for file in filenames:
    ClearFile(file)

def SimulationEnd(message_queue, flights, screens):
  """Clears message buffer, exercises histograms, and other simulation wrap up.

  Args:
    message_queue: List of flight messages that have not yet been printed.
    flights: List of flights dictionaries.
    screens: List of past screens displayed to splitflap screen.
  """
  if flights:
    histogram = {
        'type': 'both',
        'histogram':'all',
        'histogram_history':'30d',
        'histogram_max_screens': '_2',
        'histogram_data_summary': 'on'}
    message_queue.extend(TriggerHistograms(flights, histogram))

    while message_queue:
      ManageMessageQueue(message_queue, 0, {'setting_delay': 0}, screens)




                            <----SKIPPED LINES---->




    s,
    subscription_id=SUBSCRIPTION_ID,
    key=KEY,
    secret=SECRET,
    timeout=5):
  """Publishes a text string to a Vestaboard.

  The message is pushed to the vestaboard splitflap display by way of its
  web services; see https://docs.vestaboard.com/introduction for more details.

  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:
    Text string indicating how the message was displayed (web or local
    api), and / or any error messages encountered.
  """
  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)}
  body_as_json_string = json.dumps(body_as_dict) # dict to json
  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))
  failure_message = ''
  try:
    curl.perform()
    status = 'Web service published'
  except pycurl.error as e:
    timing_message = CurlTimingDetailsToString(curl)
    failure_message = (
        '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(failure_message)
    error_code = PublishMessageLocal(s, timeout=timeout, update_dashboard=False)
    if not error_code:
      status = (
          'Local service published because web service failed with %s'
          % failure_message)
    else:
      status = (
          'Local service failed with %s after web service failed with %s' %
          (error_code, failure_message))
  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:
      failure_message = (
          'Server returned HTTP status code %d for message %s; '
          'timing details: %s' % (status_code, s, timing_message))
      Log(failure_message)
      error_code = PublishMessageLocal(
          s, timeout=timeout, update_dashboard=False)
      if not error_code:
        status = (
            'Local service published because web service failed with %s'
            % failure_message)
      else:
        status = (
            'Local service failed with %s after web service failed with %s' %
            (error_code, failure_message))

  # 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:
    failure_message = ''

  curl.close()
  UpdateStatusLight(
      GPIO_ERROR_VESTABOARD_CONNECTION, error_code, failure_message)

  return status


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




                            <----SKIPPED LINES---->





  return personal_message


def ManageMessageQueue(
    message_queue, next_message_time, configuration, screens):
  """Check time & if appropriate, display next message from queue.

  Args:
    message_queue: FIFO list of message tuples of (message type,
      message string).
    next_message_time: epoch at which next message should be displayed
    configuration: dictionary of configuration attributes.
    screens: List of past screens displayed to splitflap screen.

  Returns:
    Next_message_time, potentially updated if a message has been displayed,
    or unchanged if no message was displayed.
  """
  if message_queue and (time.time() >= next_message_time or SIMULATION):
    status = ''

    if SIMULATION:  # drain the queue because the messages come so fast
      messages_to_display = list(message_queue)
      # passed by reference, so clear it out since we drained it to the display
      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):
        status = 'Message purged as no longer meets display criteria'
        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)
          status = PublishMessageWeb(splitflap_message)

      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
        # - time stamp of the recording
        # - a dictionary of elements
        data = (
            DisplayFlightNumber(flight),
            time.time(),
            {
                'message_text': message_text,
                'status': status,
                'message_type': message_type})
        PickleObjectToFile(data, PICKLE_FLIGHT_DASHBOARD, True)


    next_message_time = time.time() + configuration['setting_delay']
  return next_message_time


def DeleteMessageTypes(q, types_to_delete):
  """Delete messages from the queue if type is in the iterable types."""
  if VERBOSE:
    messages_to_delete = [m for m in q if m[0] in types_to_delete]
    if messages_to_delete:
      Log('Deleting messages from queue due to new-found plane: %s'
          % messages_to_delete)
  updated_q = [m for m in q if m[0] not in types_to_delete]
  return updated_q


def BootstrapInsightList(full_path=PICKLE_FLIGHTS):
  """(Re)populate flight pickle files with flight insight distributions.

  The set of insights generated for each flight is created at the time




                            <----SKIPPED LINES---->




  Args:
    value: Boolean indicating whether a failure has occurred (True) or
      system is nominal (False).
    subsystem: A tuple describing the system; though that description may
      have multiple attributes, the 0th element is the numeric identifier
      of that system.  monitoring.py depends on other attributes of that
      tuple being present as well.  Since the overall system does not have
      a tuple defined for it, it gets a default identifier of 0.
    failure_message: an (optional) message describing why the system /
      subsystem is being disabled or failing.
    iteration: integer indicating how many times the main loop has been
      completed.
  """
  versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO)
  if subsystem:
    subsystem = subsystem[0]
  PickleObjectToFile((
      time.time(), subsystem, value, versions,
      failure_message, INSTANCE_START_TIME,
      iteration, gpiozero.CPUTemperature().temperature),
      PICKLE_SYSTEM_DASHBOARD, True)


def RemoveFile(file):
  """Removes a file, returning a boolean indicating if it had existed."""
  if os.path.exists(file):

    try:
      os.remove(file)
    except PermissionError:
      return False
    return True

  return False


def ConfirmNewFlight(flight, flights):
  """Replaces last-seen flight with new flight if identifiers overlap.

  Flights are identified by the radio over time by a tuple of identifiers:
  flight_number and squawk.  Due to unknown communication issues, one or the




                            <----SKIPPED LINES---->




    configuration = new_configuration

    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) = 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:
        time_new_flight_found = time.time()
        flights.append(flight)
        remote, servo = RefreshArduinos(
            remote, servo,
            to_remote_q, to_servo_q, to_main_q, shutdown,
            flights, json_desc_dict, configuration, screen_history)

        flight_meets_display_criteria, reason_flight_fails_criteria = (
            FlightMeetsDisplayCriteria(flight, configuration, log=True))

        # Initialize variables for saving in case details not populated by
        # later code prior to saving in pickle
        flight_aware_error_message = ''
        time_flight_message_inserted = 0
        time_insight_message_inserted = 0

        if flight_meets_display_criteria:
          personal_message = None  # Any personal message displayed now cleared
          flight_message = (
              FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight), flight)

          # display the next message about this flight now!
          next_message_time = time.time()
          message_queue.insert(0, flight_message)
          time_flight_message_inserted = time.time()
          # and delete any queued insight messages about other flights that have
          # not yet displayed, since a newer flight has taken precedence
          message_queue = DeleteMessageTypes(message_queue, (FLAG_MSG_INSIGHT,))

          # Though we also manage the message queue outside this conditional
          # as well, because it can take a half second to generate the flight
          # insights, this allows this message to start displaying on the
          # board immediately, so it's up there when it's most relevant
          next_message_time = ManageMessageQueue(
              message_queue, next_message_time, configuration, screen_history)

          insight_messages = CreateFlightInsights(
              flights,
              configuration.get('insights'),
              insight_message_distribution)
          if configuration.get('next_flight', 'off') == 'on':
            next_flight_text = FlightInsightNextFlight(flights, configuration)
            if next_flight_text:
              insight_messages.insert(0, next_flight_text)

          insight_messages = [(FLAG_MSG_INSIGHT, m) for m in insight_messages]

          time_insight_message_inserted = time.time()
          for insight_message in insight_messages:
            message_queue.insert(0, insight_message)

        else:  # flight didn't meet display criteria
          flight['insight_types'] = []

        PickleObjectToFile(
            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_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
    # now that we've saved the flight, we have no more need to keep the
    # memory-hogging persistent_path key of that flight in live memory
    if 'persistent_path' in flights[-1]:
      del flights[-1]['persistent_path']
    # 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---->