messageboard-2020-07-12-2029.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266








331332333334335336337338339340341342343344345346347348349350 351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388  389390391392393394395396397398399400401402403404405406407408409410








22512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291








24662467246824692470247124722473247424752476247724782479248024812482248324842485               24862487248824892490249124922493249424952496    24972498249925002501250225032504250525062507250825092510251125122513251425152516








34493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489








6003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033     60346035603660376038 603960406041604260436044604560466047 60486049  60506051605260536054605560566057605860596060606160626063606460656066606760686069








65096510651165126513651465156516651765186519652065216522652365246525652665276528  65296530653165326533653465356536653765386539654065416542654365446545654665476548








65636564656565666567656865696570657165726573657465756576657765786579658065816582 65836584658565866587658865896590659165926593659465956596659765986599660066016602








660966106611661266136614661566166617661866196620662166226623662466256626662766286629 6630663166326633663466356636663766386639664066416642664366446645664666476648  66496650665166526653665466556656665766586659666066616662666366646665666666676668











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




    23,
    'ERROR: FlightAware not available',
    'SUCCESS: FlightAware available',
    2, 'FlightAware connected', False)
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = (
    24,
    'ERROR: Servos not running or lost connection',
    'SUCCESS: Handshake with servo Arduino received',
    3, 'Hemisphere connected', True)
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = (
    25,
    'ERROR: Remote not running or lost connection',
    'SUCCESS: Handshake with remote Arduino received',
    4, 'Remote connected', True)
GPIO_ERROR_BATTERY_CHARGE = (
    26,
    'ERROR: Remote battery is low',
    'SUCCESS: Remote battery recharged',
    5, 'Remote charged', False)
GPIO_FAN = (
    5,
    'ERROR: RPi above %dC degrees' % TEMP_FAN_TURN_ON_CELSIUS,
    'SUCCESS: RPi below %dC degrees' % TEMP_FAN_TURN_OFF_CELSIUS,
    7, 'Thermal condition', False)

# for future expansion
GPIO_UNUSED_1 = (
    27,
    'Undefined condition set to true',
    'Undefined condition set to false',
    6, 'Unused', False)
GPIO_UNUSED_2 = (
    6,
    'Undefined condition set to true',
    'Undefined condition set to false',
    8, 'Unused', False)

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

#if running on raspberry, then need to prepend path to file names
if RASPBERRY_PI:
  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




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




AIRCRAFT_LENGTH['Airbus A300F4-600 (twin-jet)'] = 54.08
AIRCRAFT_LENGTH['Airbus A319 (twin-jet)'] = 33.84
AIRCRAFT_LENGTH['Airbus A320 (twin-jet)'] = 37.57
AIRCRAFT_LENGTH['Airbus A320neo (twin-jet)'] = 37.57
AIRCRAFT_LENGTH['Airbus A321 (twin-jet)'] = 44.51
AIRCRAFT_LENGTH['Airbus A321neo (twin-jet)'] = 44.51
AIRCRAFT_LENGTH['Airbus A330-200 (twin-jet)'] = 58.82
AIRCRAFT_LENGTH['Airbus A330-300 (twin-jet)'] = 63.67
AIRCRAFT_LENGTH['Airbus A340-300 (quad-jet)'] = 63.69
AIRCRAFT_LENGTH['Airbus A350-1000 (twin-jet)'] = 73.79
AIRCRAFT_LENGTH['Airbus A350-900 (twin-jet)'] = 66.8
AIRCRAFT_LENGTH['Airbus A380-800 (quad-jet)'] = 72.72
AIRCRAFT_LENGTH['Beechcraft Bonanza (33) (piston-single)'] = 7.65
AIRCRAFT_LENGTH['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
AIRCRAFT_LENGTH['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
AIRCRAFT_LENGTH['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
AIRCRAFT_LENGTH['Boeing 737-400 (twin-jet)'] = 36.4
AIRCRAFT_LENGTH['Boeing 737-700 (twin-jet)'] = 33.63
AIRCRAFT_LENGTH['Boeing 737-800 (twin-jet)'] = 39.47
AIRCRAFT_LENGTH['Boeing 737-900 (twin-jet)'] = 42.11

AIRCRAFT_LENGTH['Boeing 747-400 (quad-jet)'] = 36.4
AIRCRAFT_LENGTH['Boeing 747-8 (quad-jet)'] = 76.25
AIRCRAFT_LENGTH['Boeing 757-200 (twin-jet)'] = 47.3
AIRCRAFT_LENGTH['Boeing 757-300 (twin-jet)'] = 54.4
AIRCRAFT_LENGTH['Boeing 767-200 (twin-jet)'] = 48.51
AIRCRAFT_LENGTH['Boeing 767-300 (twin-jet)'] = 54.94
AIRCRAFT_LENGTH['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
AIRCRAFT_LENGTH['Boeing 777-200 (twin-jet)'] = 63.73
AIRCRAFT_LENGTH['Boeing 777-200LR/F (twin-jet)'] = 63.73
AIRCRAFT_LENGTH['Boeing 777-300ER (twin-jet)'] = 73.86
AIRCRAFT_LENGTH['Boeing 787-10 (twin-jet)'] = 68.28
AIRCRAFT_LENGTH['Boeing 787-8 (twin-jet)'] = 56.72
AIRCRAFT_LENGTH['Boeing 787-9 (twin-jet)'] = 62.81
AIRCRAFT_LENGTH['Bombardier Challenger 300 (twin-jet)'] = 20.92
AIRCRAFT_LENGTH['Bombardier Global 5000 (twin-jet)'] = 29.5
AIRCRAFT_LENGTH['Bombardier Global Express (twin-jet)'] = (29.5 + 30.3) / 2
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
AIRCRAFT_LENGTH['Canadair Challenger (twin-jet)'] = 20.9
AIRCRAFT_LENGTH['Canadair Challenger 350 (twin-jet)'] = 20.9
AIRCRAFT_LENGTH['Cessna Caravan (single-turboprop)'] = 11.46
AIRCRAFT_LENGTH['Cessna Citation CJ2+ (twin-jet)'] = 14.53
AIRCRAFT_LENGTH['Cessna Citation CJ3 (twin-jet)'] = 15.59
AIRCRAFT_LENGTH['Cessna Citation Excel/XLS (twin-jet)'] = 16.0
AIRCRAFT_LENGTH['Cessna Citation II (twin-jet)'] = 14.54
AIRCRAFT_LENGTH['Cessna Citation Latitude (twin-jet)'] = 18.97
AIRCRAFT_LENGTH['Cessna Citation Sovereign (twin-jet)'] = 19.35
AIRCRAFT_LENGTH['Cessna Citation V (twin-jet)'] = 14.91
AIRCRAFT_LENGTH['Cessna Citation X (twin-jet)'] = 22.04
AIRCRAFT_LENGTH['Cessna Citation Mustang (twin-jet)'] = 12.37
AIRCRAFT_LENGTH['Cessna Skyhawk (piston-single)'] = 8.28
AIRCRAFT_LENGTH['Cessna Skylane (piston-single)'] = 8.84
AIRCRAFT_LENGTH['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
AIRCRAFT_LENGTH['Cirrus SR-22 (piston-single)'] = 7.92
AIRCRAFT_LENGTH['Dassault Falcon 900 (tri-jet)'] = 20.21
AIRCRAFT_LENGTH['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
AIRCRAFT_LENGTH['Embraer Phenom 300 (twin-jet)'] = 15.9


AIRCRAFT_LENGTH['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
AIRCRAFT_LENGTH['Embraer ERJ-135 (twin-jet)'] = 26.33
AIRCRAFT_LENGTH['Gulfstream Aerospace Gulfstream G550 (twin-jet)'] = 29.39
AIRCRAFT_LENGTH['Gulfstream Aerospace Gulfstream IV (twin-jet)'] = 26.92
AIRCRAFT_LENGTH['Learjet 45 (twin-jet)'] = 17.68
AIRCRAFT_LENGTH['McDonnell Douglas MD-11 (tri-jet)'] = 61.6
AIRCRAFT_LENGTH['Pilatus PC-12 (single-turboprop)'] = 14.4


def Log(message, file=None, rolling=None):
  """Write a message to a logfile along with a timestamp.

  Args:
    message: string message to write
    file: string representing file name and, if needed, path to the
      file to write to
    rolling: name of file that will keep only the last n files of file
  """
  # can't define as a default parameter because LOGFILE name is potentially
  # modified based on SIMULATION flag
  if not file:
    file = LOGFILE




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




      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' %




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





  Args:
    lines: list of strings that comprise the message
    splitflap: boolean, True if directed for splitflap display; false if
    directed to screen

  Returns:
    String - which includes embedded new line characters, borders, etc. as
    described above, that can be printed to screen as the message.
  """
  divider = '+' + '-'*SPLITFLAP_CHARS_PER_LINE + '+'
  border_character = '|'
  append_character = '\n'

  if splitflap:
    border_character = ''
    append_character = ''

  for unused_n in range(SPLITFLAP_LINE_COUNT-len(lines)):
    lines.append('')















  lines = [
      border_character +
      line.ljust(SPLITFLAP_CHARS_PER_LINE).upper() +
      border_character
      for line in lines]

  if not splitflap:
    lines.insert(0, divider)
    lines.append(divider)

  return append_character.join(lines)






def FlightInsightLastSeen(flights, days_ago=2):
  """Generates string indicating when flight was last seen.

  Generates text of the following form.
  - KAL214 was last seen 2d0h ago

  Args:
    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0]
      was the earliest seen, and flights[-1] is the most recent flight for
      which we are attempting to generate an insight.
    days_ago: the minimum time difference for which a message should be
      generated - i.e.: many flights are daily, and so we are not necessarily
      interested to see about every daily flight that it was seen yesterday.
      However, more infrequent flights might be of interest.

  Returns:
    Printable string message; if no message or insights to generate, then an




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




      last_potential_observation_string = SecondsToDdHh(
          last_potential_observation_sec)
      if matching:
        message = '%s is the first time %s %s%s has been seen since %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)
      else:
        message = (
            '%s is the first time %s %s%s has been '
            'seen since at least %s ago' % (
                this_flight_number, label, this_instance, additional_descriptor,
                last_potential_observation_string))

  return message


def FlightInsightSuperlativeVertrate(flights, hours=HOURS_IN_DAY):
  """Generates string about the climb rate of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - UAL631   has the fastest ascent rate (5248fpm, 64fpm faster than next
    fastest) in last 24 hours
  - CKS1820 has the fastest descent rate (-1152fpm, -1088fpm faster than next
    fastest) in last 24 hours

  While this is conceptually similar to the more generic
  FlightInsightSuperlativeVertrate function, vert_rate - because it can be
  either positive or negative, with different signs requiring different
  labeling and comparisons - it needs its own special handling.

  Args:
    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0]
      was the earliest seen, and flights[-1] is the most recent flight for
      which we are attempting to generate an insight.
    hours: the time horizon over which to look for superlative flights.

  Returns:
    Printable string message; if no message or insights to generate, then an
    empty string.
  """




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




      pointer += 1
    elif s[pointer] == open_escape_char:
      end = s.find(close_escape_char, pointer)
      if end == -1:  # open escape did not close
        pointer = len(s)
      else:
        try:
          escape_value = int(s[pointer+1:end])
        except ValueError:
          escape_value = None
        if escape_value in valid_escape_values:
          validated_s += s[pointer:end+1]
          l += 1
        pointer = end
    else:
      pointer += 1

  return validated_s


def PersonalMessage(configuration, message_queue):
  """Formats and displays the personal message.

  A user-defined message can be displayed to the board whenever there isn't a
  flight message during user-specified hours of the day. This clears the
  board, if requested, and then adds that message to the queue.

  Args:
    configuration: the settings dictionary.
    message_queue: the existing queue, to which the personal message - if
      any - is added.





  """
  if 'clear_board' in configuration:
    RemoveSetting(configuration, 'clear_board')
    message_queue.append((FLAG_MSG_CLEAR, ''))
  minute_of_day = MinuteOfDay()

  if (
      not message_queue and
      'personal_message_enabled' in configuration and
      configuration['personal_message'] and
      minute_of_day <= configuration['personal_off_time'] and
      minute_of_day > configuration['personal_on_time'] + 1):
    message = configuration['personal_message']
    lines = [TruncateEscapedLine(l) for l in
             message.split('\n')[:SPLITFLAP_LINE_COUNT]]

    message_queue.append((FLAG_MSG_PERSONAL, lines))
    Log('Personal message added to queue: %s' % str(lines))




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




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




  histogram = {}

  # Next up to print is index 0; this is a list of tuples:
  # tuple element#1: flag indicating the type of message that this is
  # tuple element#2: the message itself
  message_queue = []
  next_message_time = time.time()

  # We repeat the loop every x seconds; this ensures that if the processing
  # time is long, we don't wait another x seconds after processing completes
  next_loop_time = time.time() + LOOP_DELAY_SECONDS

  # These files are read only if the version on disk has been modified more
  # recently than the last time it was read
  last_dump_json_timestamp = 0

  init_timing.append((time.time(), 7))
  WaitUntilKillComplete(already_running_ids)
  init_timing.append((time.time(), 8))



  LogTimes(init_timing)

  Log('Finishing initialization of %d; starting radio polling loop' %
      os.getpid())
  while ((not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS))
         and not SHUTDOWN_SIGNAL):

    last_heartbeat_time = Heartbeat(last_heartbeat_time)

    new_configuration = ReadAndParseSettings(CONFIG_FILE)
    UpdateRollingLogSize(new_configuration)
    CheckForNewFilterCriteria(
        configuration, new_configuration, message_queue, flights)
    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




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




       flight, now,
       json_desc_dict,
       persistent_path) = ScanForNewFlights(
           persistent_nearby_aircraft,
           persistent_path,
           configuration.get('log_jsons', False))

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

          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'),




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




          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)

    PersonalMessage(configuration, message_queue)


    if SIMULATION:
      if now:
        simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z')
      if simulated_hour != prev_simulated_hour:
        print(simulated_hour)
        prev_simulated_hour = simulated_hour

    histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
    RemoveFile(HISTOGRAM_CONFIG_FILE)

    # We also need to make sure there are flights on which to generate a
    # histogram! Why might there not be any flights? Primarily during a
    # simulation, if there's a lingering histogram file at the time of
    # history restart.
    if histogram and not flights:
      Log('Histogram requested (%s) but no flights in memory' % histogram)
    if histogram and flights:
      message_queue.extend(TriggerHistograms(flights, histogram))



    # check time & if appropriate, display next message from queue
    next_message_time = ManageMessageQueue(
        message_queue, next_message_time, configuration, screen_history)

    reboot = CheckRebootNeeded(
        startup_time, message_queue, json_desc_dict, configuration)

    CheckTemperature()

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1
      if simulation_slowdown:
        SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)

  if SIMULATION:
    SimulationEnd(message_queue, flights, screen_history)




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





01234567890123456789012345678901234567890123456789012345678901234567890123456789









219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266








331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 393394395396397398399400401402403404405406407408409410411412








22532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293








2468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537








34703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510








6024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099








653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580








65956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635








664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704











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




    23,
    'ERROR: FlightAware not available',
    'SUCCESS: FlightAware available',
    2, 'FlightAware connected', False)
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = (
    24,
    'ERROR: Servos not running or lost connection',
    'SUCCESS: Handshake with servo Arduino received',
    3, 'Hemisphere connected', True)
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = (
    25,
    'ERROR: Remote not running or lost connection',
    'SUCCESS: Handshake with remote Arduino received',
    4, 'Remote connected', True)
GPIO_ERROR_BATTERY_CHARGE = (
    26,
    'ERROR: Remote battery is low',
    'SUCCESS: Remote battery recharged',
    5, 'Remote charged', False)
GPIO_FAN = (
    27,
    'ERROR: RPi above %dC degrees' % TEMP_FAN_TURN_ON_CELSIUS,
    'SUCCESS: RPi below %dC degrees' % TEMP_FAN_TURN_OFF_CELSIUS,
    7, 'Thermal condition', False)

# for future expansion
GPIO_UNUSED_1 = (
    5,
    'Undefined condition set to true',
    'Undefined condition set to false',
    6, 'Unused', False)
GPIO_UNUSED_2 = (
    6,
    'Undefined condition set to true',
    'Undefined condition set to false',
    8, 'Unused', False)

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

#if running on raspberry, then need to prepend path to file names
if RASPBERRY_PI:
  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




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




AIRCRAFT_LENGTH['Airbus A300F4-600 (twin-jet)'] = 54.08
AIRCRAFT_LENGTH['Airbus A319 (twin-jet)'] = 33.84
AIRCRAFT_LENGTH['Airbus A320 (twin-jet)'] = 37.57
AIRCRAFT_LENGTH['Airbus A320neo (twin-jet)'] = 37.57
AIRCRAFT_LENGTH['Airbus A321 (twin-jet)'] = 44.51
AIRCRAFT_LENGTH['Airbus A321neo (twin-jet)'] = 44.51
AIRCRAFT_LENGTH['Airbus A330-200 (twin-jet)'] = 58.82
AIRCRAFT_LENGTH['Airbus A330-300 (twin-jet)'] = 63.67
AIRCRAFT_LENGTH['Airbus A340-300 (quad-jet)'] = 63.69
AIRCRAFT_LENGTH['Airbus A350-1000 (twin-jet)'] = 73.79
AIRCRAFT_LENGTH['Airbus A350-900 (twin-jet)'] = 66.8
AIRCRAFT_LENGTH['Airbus A380-800 (quad-jet)'] = 72.72
AIRCRAFT_LENGTH['Beechcraft Bonanza (33) (piston-single)'] = 7.65
AIRCRAFT_LENGTH['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
AIRCRAFT_LENGTH['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
AIRCRAFT_LENGTH['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
AIRCRAFT_LENGTH['Boeing 737-400 (twin-jet)'] = 36.4
AIRCRAFT_LENGTH['Boeing 737-700 (twin-jet)'] = 33.63
AIRCRAFT_LENGTH['Boeing 737-800 (twin-jet)'] = 39.47
AIRCRAFT_LENGTH['Boeing 737-900 (twin-jet)'] = 42.11
AIRCRAFT_LENGTH['Boeing 747-100 (quad-jet)'] = 70.66
AIRCRAFT_LENGTH['Boeing 747-400 (quad-jet)'] = 70.66
AIRCRAFT_LENGTH['Boeing 747-8 (quad-jet)'] = 76.25
AIRCRAFT_LENGTH['Boeing 757-200 (twin-jet)'] = 47.3
AIRCRAFT_LENGTH['Boeing 757-300 (twin-jet)'] = 54.4
AIRCRAFT_LENGTH['Boeing 767-200 (twin-jet)'] = 48.51
AIRCRAFT_LENGTH['Boeing 767-300 (twin-jet)'] = 54.94
AIRCRAFT_LENGTH['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
AIRCRAFT_LENGTH['Boeing 777-200 (twin-jet)'] = 63.73
AIRCRAFT_LENGTH['Boeing 777-200LR/F (twin-jet)'] = 63.73
AIRCRAFT_LENGTH['Boeing 777-300ER (twin-jet)'] = 73.86
AIRCRAFT_LENGTH['Boeing 787-10 (twin-jet)'] = 68.28
AIRCRAFT_LENGTH['Boeing 787-8 (twin-jet)'] = 56.72
AIRCRAFT_LENGTH['Boeing 787-9 (twin-jet)'] = 62.81
AIRCRAFT_LENGTH['Bombardier Challenger 300 (twin-jet)'] = 20.92
AIRCRAFT_LENGTH['Bombardier Global 5000 (twin-jet)'] = 29.5
AIRCRAFT_LENGTH['Bombardier Global Express (twin-jet)'] = (29.5 + 30.3) / 2
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
AIRCRAFT_LENGTH['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
AIRCRAFT_LENGTH['Canadair Challenger (twin-jet)'] = 20.9
AIRCRAFT_LENGTH['Canadair Challenger 350 (twin-jet)'] = 20.9
AIRCRAFT_LENGTH['Cessna Caravan (single-turboprop)'] = 11.46
AIRCRAFT_LENGTH['Cessna Citation CJ2+ (twin-jet)'] = 14.53
AIRCRAFT_LENGTH['Cessna Citation CJ3 (twin-jet)'] = 15.59
AIRCRAFT_LENGTH['Cessna Citation Excel/XLS (twin-jet)'] = 16.0
AIRCRAFT_LENGTH['Cessna Citation II (twin-jet)'] = 14.54
AIRCRAFT_LENGTH['Cessna Citation Latitude (twin-jet)'] = 18.97
AIRCRAFT_LENGTH['Cessna Citation Sovereign (twin-jet)'] = 19.35
AIRCRAFT_LENGTH['Cessna Citation V (twin-jet)'] = 14.91
AIRCRAFT_LENGTH['Cessna Citation X (twin-jet)'] = 22.04
AIRCRAFT_LENGTH['Cessna Citation Mustang (twin-jet)'] = 12.37
AIRCRAFT_LENGTH['Cessna Skyhawk (piston-single)'] = 8.28
AIRCRAFT_LENGTH['Cessna Skylane (piston-single)'] = 8.84
AIRCRAFT_LENGTH['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
AIRCRAFT_LENGTH['Cirrus SR-22 (piston-single)'] = 7.92
AIRCRAFT_LENGTH['Dassault Falcon 900 (tri-jet)'] = 20.21
AIRCRAFT_LENGTH['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
AIRCRAFT_LENGTH['Embraer Phenom 300 (twin-jet)'] = 15.9
AIRCRAFT_LENGTH['Embraer ERJ-135 (twin-jet)'] = 26.33
AIRCRAFT_LENGTH['Embraer ERJ-145 (twin-jet)'] = 29.87
AIRCRAFT_LENGTH['EMBRAER 175 (long wing) (twin-jet)'] = 31.68

AIRCRAFT_LENGTH['Gulfstream Aerospace Gulfstream G550 (twin-jet)'] = 29.39
AIRCRAFT_LENGTH['Gulfstream Aerospace Gulfstream IV (twin-jet)'] = 26.92
AIRCRAFT_LENGTH['Learjet 45 (twin-jet)'] = 17.68
AIRCRAFT_LENGTH['McDonnell Douglas MD-11 (tri-jet)'] = 61.6
AIRCRAFT_LENGTH['Pilatus PC-12 (single-turboprop)'] = 14.4


def Log(message, file=None, rolling=None):
  """Write a message to a logfile along with a timestamp.

  Args:
    message: string message to write
    file: string representing file name and, if needed, path to the
      file to write to
    rolling: name of file that will keep only the last n files of file
  """
  # can't define as a default parameter because LOGFILE name is potentially
  # modified based on SIMULATION flag
  if not file:
    file = LOGFILE




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




      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' %




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





  Args:
    lines: list of strings that comprise the message
    splitflap: boolean, True if directed for splitflap display; false if
    directed to screen

  Returns:
    String - which includes embedded new line characters, borders, etc. as
    described above, that can be printed to screen as the message.
  """
  divider = '+' + '-'*SPLITFLAP_CHARS_PER_LINE + '+'
  border_character = '|'
  append_character = '\n'

  if splitflap:
    border_character = ''
    append_character = ''

  for unused_n in range(SPLITFLAP_LINE_COUNT-len(lines)):
    lines.append('')

  # convert escaped character codes potentially embedded in personal
  # message to something that can be displayed well on screen, albeit
  # the colors for {63}..{69} aren't captured in plain text
  # see https://docs.vestaboard.com/characters
  escaped_characters = (
      ('{62}', u'\u00b0'),
      ('{63}', u'\u2588'),  # red
      ('{64}', u'\u2588'),  # orange
      ('{65}', u'\u2588'),  # yellow
      ('{66}', u'\u2588'),  # green
      ('{67}', u'\u2588'),  # blue
      ('{68}', u'\u2588'),  # violet
      ('{69}', u'\u2588'))  # red

  lines = [
      border_character +
      line.ljust(SPLITFLAP_CHARS_PER_LINE).upper() +
      border_character
      for line in lines]

  if not splitflap:
    lines.insert(0, divider)
    lines.append(divider)

  message = append_character.join(lines)
  for code, character in escaped_characters:
    message = message.replace(code, character)

  return message


def FlightInsightLastSeen(flights, days_ago=2):
  """Generates string indicating when flight was last seen.

  Generates text of the following form.
  - KAL214 was last seen 2d0h ago

  Args:
    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0]
      was the earliest seen, and flights[-1] is the most recent flight for
      which we are attempting to generate an insight.
    days_ago: the minimum time difference for which a message should be
      generated - i.e.: many flights are daily, and so we are not necessarily
      interested to see about every daily flight that it was seen yesterday.
      However, more infrequent flights might be of interest.

  Returns:
    Printable string message; if no message or insights to generate, then an




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




      last_potential_observation_string = SecondsToDdHh(
          last_potential_observation_sec)
      if matching:
        message = '%s is the first time %s %s%s has been seen since %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)
      else:
        message = (
            '%s is the first time %s %s%s has been '
            'seen since at least %s ago' % (
                this_flight_number, label, this_instance, additional_descriptor,
                last_potential_observation_string))

  return message


def FlightInsightSuperlativeVertrate(flights, hours=HOURS_IN_DAY):
  """Generates string about the climb rate of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - UAL631 has the fastest ascent rate (5248fpm, 64fpm faster than next
    fastest) in last 24 hours
  - CKS1820 has the fastest descent rate (-1152fpm, -1088fpm faster than next
    fastest) in last 24 hours

  While this is conceptually similar to the more generic
  FlightInsightSuperlativeVertrate function, vert_rate - because it can be
  either positive or negative, with different signs requiring different
  labeling and comparisons - it needs its own special handling.

  Args:
    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0]
      was the earliest seen, and flights[-1] is the most recent flight for
      which we are attempting to generate an insight.
    hours: the time horizon over which to look for superlative flights.

  Returns:
    Printable string message; if no message or insights to generate, then an
    empty string.
  """




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




      pointer += 1
    elif s[pointer] == open_escape_char:
      end = s.find(close_escape_char, pointer)
      if end == -1:  # open escape did not close
        pointer = len(s)
      else:
        try:
          escape_value = int(s[pointer+1:end])
        except ValueError:
          escape_value = None
        if escape_value in valid_escape_values:
          validated_s += s[pointer:end+1]
          l += 1
        pointer = end
    else:
      pointer += 1

  return validated_s


def PersonalMessage(configuration, message_queue, last_message):
  """Formats and displays the personal message.

  A user-defined message can be displayed to the board whenever there isn't a
  flight message during user-specified hours of the day. This clears the
  board, if requested, and then adds that message to the queue.

  Args:
    configuration: the settings dictionary.
    message_queue: the existing queue, to which the personal message - if
      any - is added.
    last_message: last message sent to screen so that we do not re-display
      a personal message unnecessarily.

  Returns:
    String message from configuration file.
  """
  if 'clear_board' in configuration:
    RemoveSetting(configuration, 'clear_board')
    message_queue.append((FLAG_MSG_CLEAR, ''))
  minute_of_day = MinuteOfDay()
  personal_message = configuration['personal_message']
  if (
      not message_queue and
      'personal_message_enabled' in configuration and
      configuration['personal_message'] and
      minute_of_day <= configuration['personal_off_time'] + 1 and
      minute_of_day >= configuration['personal_on_time'] and
      last_message != personal_message):
    lines = [TruncateEscapedLine(l) for l in
             personal_message.split('\n')[:SPLITFLAP_LINE_COUNT]]

    message_queue.append((FLAG_MSG_PERSONAL, lines))
    Log('Personal message added to queue: %s' % str(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




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




  histogram = {}

  # Next up to print is index 0; this is a list of tuples:
  # tuple element#1: flag indicating the type of message that this is
  # tuple element#2: the message itself
  message_queue = []
  next_message_time = time.time()

  # We repeat the loop every x seconds; this ensures that if the processing
  # time is long, we don't wait another x seconds after processing completes
  next_loop_time = time.time() + LOOP_DELAY_SECONDS

  # These files are read only if the version on disk has been modified more
  # recently than the last time it was read
  last_dump_json_timestamp = 0

  init_timing.append((time.time(), 7))
  WaitUntilKillComplete(already_running_ids)
  init_timing.append((time.time(), 8))

  personal_message = None  # Unknown what personal message is displayed

  LogTimes(init_timing)

  Log('Finishing initialization of %d; starting radio polling loop' %
      os.getpid())
  while ((not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS))
         and not SHUTDOWN_SIGNAL):

    last_heartbeat_time = Heartbeat(last_heartbeat_time)

    new_configuration = ReadAndParseSettings(CONFIG_FILE)
    UpdateRollingLogSize(new_configuration)
    CheckForNewFilterCriteria(
        configuration, new_configuration, message_queue, flights)
    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




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




       flight, now,
       json_desc_dict,
       persistent_path) = ScanForNewFlights(
           persistent_nearby_aircraft,
           persistent_path,
           configuration.get('log_jsons', False))

      # 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'),




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




          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)

    if SIMULATION:
      if now:
        simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z')
      if simulated_hour != prev_simulated_hour:
        print(simulated_hour)
        prev_simulated_hour = simulated_hour

    histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
    RemoveFile(HISTOGRAM_CONFIG_FILE)

    # We also need to make sure there are flights on which to generate a
    # histogram! Why might there not be any flights? Primarily during a
    # simulation, if there's a lingering histogram file at the time of
    # history restart.
    if histogram and not flights:
      Log('Histogram requested (%s) but no flights in memory' % histogram)
    if histogram and flights:
      message_queue.extend(TriggerHistograms(flights, histogram))
      if message_queue:  # Any personal message displayed has been cleared
        personal_message = None

    # check time & if appropriate, display next message from queue
    next_message_time = ManageMessageQueue(
        message_queue, next_message_time, configuration, screen_history)

    reboot = CheckRebootNeeded(
        startup_time, message_queue, json_desc_dict, configuration)

    CheckTemperature()

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1
      if simulation_slowdown:
        SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)

  if SIMULATION:
    SimulationEnd(message_queue, flights, screen_history)




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