messageboard-2020-06-11-1237.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









7273747576777879808182838485868788899091929394   9596979899100101102103104105106107108109110111112113114








227228229230231232233234235236237238239240241242243244245246 247248249250251252253254255256257258259260261262263264265266








13911392139313941395139613971398139914001401140214031404140514061407140814091410         14111412141314141415141614171418141914201421142214231424142514261427142814291430








5253525452555256525752585259526052615262526352645265526652675268526952705271527252735274                    52755276 527752785279528052815282528352845285528652875288528952905291529252935294529552965297








53275328532953305331533253335334533553365337533853395340534153425343534453455346               53475348           5349535053515352535353545355535653575358535953605361536253635364   53655366536753685369537053715372537353745375537653775378537953805381538253835384








5395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437








55985599560056015602560356045605560656075608560956105611561256135614561556165617    5618561956205621562256235624          56255626562756285629563056315632563356345635563656375638563956405641564256435644








59505951595259535954595559565957595859595960596159625963596459655966596759685969  59705971597259735974597559765977597859795980598159825983598459855986598759885989








60146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153











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




# number of seconds to wait between recording heartbeats to the status file
HEARTBEAT_SECONDS = 10

# version control directory
CODE_REPOSITORY = ''
VERSION_REPOSITORY = 'versions/'
VERSION_WEBSITE_PATH = VERSION_REPOSITORY
VERSION_MESSAGEBOARD = None
VERSION_ARDUINO = None

MAX_INSIGHT_HORIZON_DAYS = 31  # histogram logic truncates to exactly 30 days of hours

# This file is where the radio drops its json file
DUMP_JSON_FILE = '/run/readsb/aircraft.json'

# At the time a flight is first identified as being of interest (in that it falls
# within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware,
# if any - is appended to the end of this pickle file. However, since this file is
# cached in working memory, flights older than 30 days are flushed from this periodically.
PICKLE_FLIGHTS = 'pickle/flights.pk'
# True splits all the flights created in simulation into separate date files, just like
# the non-simulated runs; False consolidates all flights into one pickle file.
SPLIT_SIMULATION_FLIGHT_PICKLE = False




# 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 = 'rolling_log.txt' #file for error messages

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

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




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




    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

  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

  HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML
  HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE
  VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY



TIMEZONE = 'US/Pacific' # timezone of display
TZ = pytz.timezone(TIMEZONE)




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




    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]
  current_distances = [
      d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None]
  if current_distances:
    json_desc_dict['radio_range_miles'] = max(current_distances)

  return json_desc_dict











def MergedIdentifier(proposed_id, existing_ids):
  """Identifies what identifier to use for a flight.

  While most flights have both a squawk and a flight number, enough are missing one only
  for it to appear later to want to use a 2-tuple of both as an identifier, merging
  flights if they share a common non-null flight number and/or squawk, as the persistent
  identifier across time.

  Additionally, in very limited circumstances, a squawk may change mid-flight; in that
  case, the first alpha squawk is used.

  This function identifies which identifier to use, and which - if any - should be merged
  into that one identifier from a group of existing identifiers.

  Args:
    proposed_id: The 2-tuple of (flight_number, squawk) of the identified flight.
    existing_ids: An iterable of existing 2-tuple identifiers, some (or none) of which
      may overlap with this flight.




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




      n += 1
      time.sleep(1)
      running_parents = FindRunningParents()


def InitArduinoVariables():
  """Initializes and starts the two arduino threads with new shared-memory queues."""
  to_remote_q = multiprocessing.Queue()
  to_servo_q = multiprocessing.Queue()
  to_main_q = multiprocessing.Queue()
  shutdown_remote = multiprocessing.Value('i')  # shared flag to initiate shutdown
  shutdown_servo = multiprocessing.Value('i')  # shared flag to initiate shutdown
  shutdown = (shutdown_remote, shutdown_servo)

  return (to_remote_q, to_servo_q, to_main_q, shutdown)


def RefreshArduinos(
    remote, servo,
    to_remote_q, to_servo_q, to_main_q, shutdown,
    flights, json_desc_dict, configuration):
  """Ensure arduinos are running, restarting if needed, & send them the current message"""




















  remote, servo = ValidateArduinosRunning(
      remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration)

  EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q)
  return remote, servo


def ValidateArduinosRunning(
    remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration):
  """Ensures that each of the enabled arduinos are running, restarting if needed.

  Args:
    remote: Running remote Arduino process (or if not previously running, None value)
    servo: Running servo Arduino process (or if not previously running, None value)
    to_remote_q: Multi-processing messaging queue for one-way comm from messageboard
      to remote arduino.
    to_servo_q: Multi-processing messaging queue for one-way comm from messageboard
      to servo arduino.
    to_main_q: Multi-processing messaging queue for one-way comm from arduinos to
      messageboard.
    shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective
      arduinos when they should be shutdown.
    configuration: Dictionary of configuration settings.





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




  if not SHUTDOWN_SIGNAL:

    if not enabled:
      if p is not None:  # must have just requested a disabling of single instance
        args[2].value = 1  # trigger a shutdown on the single instance
      return None

    if p is None or not p.is_alive():
      if p is None:
        Log('Process for %s starting for first time' % str(start_function))
      elif VERBOSE:
        Log('Process (%s) for %s died; restarting' % (str(p), str(start_function)))
      args[2].value = 0  # (re)set shutdown flag to allow function to run
      p = multiprocessing.Process(target=start_function, args=args)
      p.daemon = False  # TODO: perhaps value of false will address correlated BT failures?
      p.start()

  return p

















def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q):
  """Send latest data to arduinos via their shared-memory queues"""











  last_flight = {}
  if flights:
    last_flight = flights[-1]

  if SIMULATION:
    now = json_desc_dict['now']
  else:
    now = time.time()

  additional_attributes = {}

  today = EpochDisplayTime(now, '%x')
  flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today])
  additional_attributes['flight_count_today'] = flight_count_today

  additional_attributes['simulation'] = SIMULATION




  message = (last_flight, json_desc_dict, configuration, additional_attributes)
  try:
    if 'enable_servos' in configuration:
      to_servo_q.put(message, block=False)
    if 'enable_remote' in configuration:
      to_remote_q.put(message, block=False)
  except queue.Full:
    msg = 'Message queues to Arduinos full - trigger shutdown'
    Log(msg)
    global SHUTDOWN_SIGNAL
    SHUTDOWN_SIGNAL = msg


def ProcessArduinoCommmands(q, flights, configuration, message_queue, next_message_time):
  """Executes the commands enqueued by the arduinos.

  The commands on the queue q are of the form (command, args), where command is an
  identifier indicating the type of instruction, and the args is a possibly empty tuple
  with the attributes to follow thru.




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





  Returns:
    A 2-tuple of the (possibly-updated) message_queue and next_message_time.
  """
  while not q.empty():
    command, args = q.get()

    if command == 'pin':
      UpdateStatusLight(*args)

    elif command == 'replay':
      # a command might request info about flight to be (re)displayed, irrespective of
      # whether the screen is on; if so, let's put that message at the front of the message
      # queue, and delete any subsequent messages in queue because presumably the button
      # was pushed either a) when the screen was off (so no messages in queue), or b)
      # because the screen was on, but the last flight details got lost after other screens
      # that we're no longer interested in
      messageboard_flight_index = IdentifyFlightDisplayed(
          flights, configuration, display_all_hours=True)
      if messageboard_flight_index is not None:
        message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INSIGHT]
        flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
        message_queue = [(FLAG_MSG_FLIGHT, flight_message)]
        next_message_time = time.time()

    elif command == 'histogram':
      if not flights:
        Log('Histogram requested by remote %s but no flights in memory' % str(args))
      else:
        histogram_type, histogram_history = args
        message_queue.extend(MessageboardHistograms(
            flights,
            histogram_type,
            histogram_history,
            '_1',
            False))

    elif command == 'update_configuration':
      updated_settings = args[0]
      Log('Updated settings received from arduino: %s' % updated_settings)
      WriteFile(CONFIG_FILE, updated_settings)

    else:




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





  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:
      message_text = message[1]
      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)




      MaintainRollingWebLog(display_message, 25)
      if not SIMULATION:
        splitflap_message = Screenify(message_text, True)
        PublishMessage(splitflap_message)

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












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 the flight was
  first identified, and saved on the flight pickle. This saving allows the current
  running distribution to be recalculated very quickly, but it means that as code
  enabling new insights gets added, those historical distributions may not necessarily
  be considered correct.

  They are "correct" in the sense that that new insight was not available at the time
  that older flight was seen, but it is not correct in the sense that, because this new
  insight is starting out with an incidence in the historical data of zero, this
  new insight may be reported more frequently than desired until it "catches up".

  So this method replays the flight history with the latest insight code, regenerating
  the insight distribution for each flight.
  """
  directory, file = os.path.split(full_path)




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




      os.kill(pid, signal.SIGTERM)
  init_timing.append((time.time(), 2))

  SetPinMode()

  configuration = ReadAndParseSettings(CONFIG_FILE)
  Log('Read CONFIG_FILE at %s: %s' % (CONFIG_FILE, str(configuration)))

  startup_time = time.time()
  json_desc_dict = {}

  init_timing.append((time.time(), 3))
  flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS)
  # Clear the loaded flight of any cached data, identified by keys with a specific
  # suffix, since code fixes may change the values for some of those cached elements
  for flight in flights:
    for key in list(flight.keys()):
      if key.endswith(CACHED_ELEMENT_PREFIX):
        flight.pop(key)
  init_timing.append((time.time(), 4))



  # If we're displaying just a single insight message, we want it to be something
  # unique, to the extent possible; this dict holds a count of the diff types of messages
  # displayed so far
  insight_message_distribution = {}

  # bootstrap the flight insights distribution from a list of insights on each
  # flight (i.e.: flight['insight_types'] for a given flight might look like
  # [1, 2, 7, 9], or [], to indicate which insights were identified; this then
  # transforms that into {0: 25, 1: 18, ...} summing across all flights.
  missing_insights = []
  for flight in flights:
    if 'insight_types' not in flight:
      missing_insights.append(
          '%s on %s' % (DisplayFlightNumber(flight), DisplayTime(flight, '%x %X')))
    distribution = flight.get('insight_types', [])
    for key in distribution:
      insight_message_distribution[key] = (
          insight_message_distribution.get(key, 0) + 1)
  if missing_insights:




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




  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(), 6))
  WaitUntilKillComplete(already_running_ids)
  init_timing.append((time.time(), 7))

  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)




    if 'setting_screen_enabled' not in configuration and 'setting_screen_enabled' in new_configuration:
      Log('setting_screen_enabled changed from NOT PRESENT to ON')
    if 'setting_screen_enabled' in configuration and 'setting_screen_enabled' not in new_configuration:
      Log('setting_screen_enabled changed from ON to NOT PRESENT')
    last_configuration = configuration







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

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

        flight_meets_display_criteria = FlightMeetsDisplayCriteria(
            flight, configuration, log=True)
        if flight_meets_display_criteria:
          flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(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
          messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INSIGHT]
          if messages_to_delete and VERBOSE:
            Log(
                'Deleting messages from queue due to new-found plane: %s' %
                messages_to_delete)
          message_queue = [m for m in message_queue if m[0] != 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)

          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)

    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:




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





01234567890123456789012345678901234567890123456789012345678901234567890123456789









72737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117








230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270








1395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443








526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331








536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447








5458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500








5661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721








602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068








60936094609560966097609860996100610161026103610461056106610761086109611061116112                6113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161     61626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211











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




# number of seconds to wait between recording heartbeats to the status file
HEARTBEAT_SECONDS = 10

# version control directory
CODE_REPOSITORY = ''
VERSION_REPOSITORY = 'versions/'
VERSION_WEBSITE_PATH = VERSION_REPOSITORY
VERSION_MESSAGEBOARD = None
VERSION_ARDUINO = None

MAX_INSIGHT_HORIZON_DAYS = 31  # histogram logic truncates to exactly 30 days of hours

# This file is where the radio drops its json file
DUMP_JSON_FILE = '/run/readsb/aircraft.json'

# At the time a flight is first identified as being of interest (in that it falls
# within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware,
# if any - is appended to the end of this pickle file. However, since this file is
# cached in working memory, flights older than 30 days are flushed from this periodically.
PICKLE_FLIGHTS = 'pickle/flights.pk'

# This allows us to identify the full history (including what was last sent to) the
# splitflap display in a programmatic fashion. While it may be interesting in its own
# right, its real use is to handle the "replay" button, so we know to enable it if what
# is displayed is the last flight.
PICKLE_SCREENS = 'pickle/screens.pk'

# Status data about messageboard - is it running, etc.  Specifically, has tuples
# of data (timestamp, system_id, status), where system_id is either the pin id of GPIO,
# or a 0 to indicate overall system, and status is boolean
PICKLE_DASHBOARD = 'pickle/dashboard.pk'

CACHED_ELEMENT_PREFIX = 'cached_'

# 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 = 'rolling_log.txt' #file for error messages

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

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




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




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

  HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML
  HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE
  VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY



TIMEZONE = 'US/Pacific' # timezone of display
TZ = pytz.timezone(TIMEZONE)




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




    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]
  current_distances = [
      d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None]
  if current_distances:
    json_desc_dict['radio_range_miles'] = max(current_distances)

  return json_desc_dict


def SameFlight(f1, f2):
  """True if these two flights are likely the same flight, False otherwise."""
  if f1['flight_number'] == f2['flight_number']:
    return True
  if f1['squawk'] == f2['squawk']:
    return True
  return False


def MergedIdentifier(proposed_id, existing_ids):
  """Identifies what identifier to use for a flight.

  While most flights have both a squawk and a flight number, enough are missing one only
  for it to appear later to want to use a 2-tuple of both as an identifier, merging
  flights if they share a common non-null flight number and/or squawk, as the persistent
  identifier across time.

  Additionally, in very limited circumstances, a squawk may change mid-flight; in that
  case, the first alpha squawk is used.

  This function identifies which identifier to use, and which - if any - should be merged
  into that one identifier from a group of existing identifiers.

  Args:
    proposed_id: The 2-tuple of (flight_number, squawk) of the identified flight.
    existing_ids: An iterable of existing 2-tuple identifiers, some (or none) of which
      may overlap with this flight.




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




      n += 1
      time.sleep(1)
      running_parents = FindRunningParents()


def InitArduinoVariables():
  """Initializes and starts the two arduino threads with new shared-memory queues."""
  to_remote_q = multiprocessing.Queue()
  to_servo_q = multiprocessing.Queue()
  to_main_q = multiprocessing.Queue()
  shutdown_remote = multiprocessing.Value('i')  # shared flag to initiate shutdown
  shutdown_servo = multiprocessing.Value('i')  # shared flag to initiate shutdown
  shutdown = (shutdown_remote, shutdown_servo)

  return (to_remote_q, to_servo_q, to_main_q, shutdown)


def RefreshArduinos(
    remote, servo,
    to_remote_q, to_servo_q, to_main_q, shutdown,
    flights, json_desc_dict, configuration, screen_history):
  """Ensure arduinos are running, restarting if needed, & send them the current message.

  Args:
    remote: Running remote Arduino process (or if not previously running, None value)
    servo: Running servo Arduino process (or if not previously running, None value)
    to_remote_q: Multi-processing messaging queue for one-way comm from messageboard
      to remote arduino.
    to_servo_q: Multi-processing messaging queue for one-way comm from messageboard
      to servo arduino.
    to_main_q: Multi-processing messaging queue for one-way comm from arduinos to
      messageboard.
    shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective
      arduinos when they should be shutdown.
    flights: List of all flights.
    json_desc_dict: Dictionary of additional attributes about radio.
    configuration: Dictionary of configuration settings.
    screen_history: List of past screens displayed to splitflap screen.

  Returns:
    A 2-tuple of the remote and servo running processes.
  """
  remote, servo = ValidateArduinosRunning(
      remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration)
  EnqueueArduinos(
      flights, json_desc_dict, configuration, to_servo_q, to_remote_q, screen_history)
  return remote, servo


def ValidateArduinosRunning(
    remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration):
  """Ensures that each of the enabled arduinos are running, restarting if needed.

  Args:
    remote: Running remote Arduino process (or if not previously running, None value)
    servo: Running servo Arduino process (or if not previously running, None value)
    to_remote_q: Multi-processing messaging queue for one-way comm from messageboard
      to remote arduino.
    to_servo_q: Multi-processing messaging queue for one-way comm from messageboard
      to servo arduino.
    to_main_q: Multi-processing messaging queue for one-way comm from arduinos to
      messageboard.
    shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective
      arduinos when they should be shutdown.
    configuration: Dictionary of configuration settings.





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




  if not SHUTDOWN_SIGNAL:

    if not enabled:
      if p is not None:  # must have just requested a disabling of single instance
        args[2].value = 1  # trigger a shutdown on the single instance
      return None

    if p is None or not p.is_alive():
      if p is None:
        Log('Process for %s starting for first time' % str(start_function))
      elif VERBOSE:
        Log('Process (%s) for %s died; restarting' % (str(p), str(start_function)))
      args[2].value = 0  # (re)set shutdown flag to allow function to run
      p = multiprocessing.Process(target=start_function, args=args)
      p.daemon = False  # TODO: perhaps value of false will address correlated BT failures?
      p.start()

  return p


def LastFlightAvailable(flights, screen_history):
  """Returns True if last message sent to splitflap is not the last flight; else False."""
  if not screen_history:
    return False

  last_message_tuple = screen_history[-1]
  last_message_type = last_message_tuple[0]
  if last_message_type == FLAG_MSG_FLIGHT:
    last_message_flight = last_message_tuple[2]
    if SameFlight(last_message_flight, flights[-1]):
      return False  # already displaying the last flight!
  return True


def EnqueueArduinos(
    flights, json_desc_dict, configuration, to_servo_q, to_remote_q, screen_history):
  """Send latest data to arduinos via their shared-memory queues.

  Args:
    flights: List of all flights.
    json_desc_dict: Dictionary of additional attributes about radio.
    configuration: Dictionary of configuration settings.
    to_servo_q: Multi-processing messaging queue for one-way comm from messageboard
      to servo arduino.
    to_remote_q: Multi-processing messaging queue for one-way comm from messageboard
      to remote arduino.
    screen_history: List of past screens displayed to splitflap screen.
  """
  last_flight = {}
  if flights:
    last_flight = flights[-1]

  if SIMULATION:
    now = json_desc_dict['now']
  else:
    now = time.time()

  additional_attributes = {}

  today = EpochDisplayTime(now, '%x')
  flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today])
  additional_attributes['flight_count_today'] = flight_count_today

  additional_attributes['simulation'] = SIMULATION

  additional_attributes['last_flight_available'] = LastFlightAvailable(
      flights, screen_history)

  message = (last_flight, json_desc_dict, configuration, additional_attributes)
  try:
    if 'enable_servos' in configuration:
      to_servo_q.put(message, block=False)
    if 'enable_remote' in configuration:
      to_remote_q.put(message, block=False)
  except queue.Full:
    msg = 'Message queues to Arduinos full - trigger shutdown'
    Log(msg)
    global SHUTDOWN_SIGNAL
    SHUTDOWN_SIGNAL = msg


def ProcessArduinoCommmands(q, flights, configuration, message_queue, next_message_time):
  """Executes the commands enqueued by the arduinos.

  The commands on the queue q are of the form (command, args), where command is an
  identifier indicating the type of instruction, and the args is a possibly empty tuple
  with the attributes to follow thru.




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





  Returns:
    A 2-tuple of the (possibly-updated) message_queue and next_message_time.
  """
  while not q.empty():
    command, args = q.get()

    if command == 'pin':
      UpdateStatusLight(*args)

    elif command == 'replay':
      # a command might request info about flight to be (re)displayed, irrespective of
      # whether the screen is on; if so, let's put that message at the front of the message
      # queue, and delete any subsequent messages in queue because presumably the button
      # was pushed either a) when the screen was off (so no messages in queue), or b)
      # because the screen was on, but the last flight details got lost after other screens
      # that we're no longer interested in
      messageboard_flight_index = IdentifyFlightDisplayed(
          flights, configuration, display_all_hours=True)
      if messageboard_flight_index is not None:
        message_queue = DeleteMessageTypes(message_queue, (FLAG_MSG_INSIGHT, ))
        flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
        message_queue.insert(0, (FLAG_MSG_FLIGHT, flight_message))
        next_message_time = time.time()

    elif command == 'histogram':
      if not flights:
        Log('Histogram requested by remote %s but no flights in memory' % str(args))
      else:
        histogram_type, histogram_history = args
        message_queue.extend(MessageboardHistograms(
            flights,
            histogram_type,
            histogram_history,
            '_1',
            False))

    elif command == 'update_configuration':
      updated_settings = args[0]
      Log('Updated settings received from arduino: %s' % updated_settings)
      WriteFile(CONFIG_FILE, updated_settings)

    else:




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





  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:
      message_text = message[1]
      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)

      # This allows us to identify whats currently on the screen
      PickleObjectToFile(message, PICKLE_SCREENS, True)

      MaintainRollingWebLog(display_message, 25)
      if not SIMULATION:
        splitflap_message = Screenify(message_text, True)
        PublishMessage(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 the flight was
  first identified, and saved on the flight pickle. This saving allows the current
  running distribution to be recalculated very quickly, but it means that as code
  enabling new insights gets added, those historical distributions may not necessarily
  be considered correct.

  They are "correct" in the sense that that new insight was not available at the time
  that older flight was seen, but it is not correct in the sense that, because this new
  insight is starting out with an incidence in the historical data of zero, this
  new insight may be reported more frequently than desired until it "catches up".

  So this method replays the flight history with the latest insight code, regenerating
  the insight distribution for each flight.
  """
  directory, file = os.path.split(full_path)




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




      os.kill(pid, signal.SIGTERM)
  init_timing.append((time.time(), 2))

  SetPinMode()

  configuration = ReadAndParseSettings(CONFIG_FILE)
  Log('Read CONFIG_FILE at %s: %s' % (CONFIG_FILE, str(configuration)))

  startup_time = time.time()
  json_desc_dict = {}

  init_timing.append((time.time(), 3))
  flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS)
  # Clear the loaded flight of any cached data, identified by keys with a specific
  # suffix, since code fixes may change the values for some of those cached elements
  for flight in flights:
    for key in list(flight.keys()):
      if key.endswith(CACHED_ELEMENT_PREFIX):
        flight.pop(key)
  init_timing.append((time.time(), 4))

  screen_history = UnpickleObjectFromFile(PICKLE_SCREENS, True, max_days=2)

  # If we're displaying just a single insight message, we want it to be something
  # unique, to the extent possible; this dict holds a count of the diff types of messages
  # displayed so far
  insight_message_distribution = {}

  # bootstrap the flight insights distribution from a list of insights on each
  # flight (i.e.: flight['insight_types'] for a given flight might look like
  # [1, 2, 7, 9], or [], to indicate which insights were identified; this then
  # transforms that into {0: 25, 1: 18, ...} summing across all flights.
  missing_insights = []
  for flight in flights:
    if 'insight_types' not in flight:
      missing_insights.append(
          '%s on %s' % (DisplayFlightNumber(flight), DisplayTime(flight, '%x %X')))
    distribution = flight.get('insight_types', [])
    for key in distribution:
      insight_message_distribution[key] = (
          insight_message_distribution.get(key, 0) + 1)
  if missing_insights:




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




  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(), 6))
  WaitUntilKillComplete(already_running_ids)
  init_timing.append((time.time(), 7))

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

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

        flight_meets_display_criteria = FlightMeetsDisplayCriteria(
            flight, configuration, log=True)
        if flight_meets_display_criteria:
          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)

          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)

    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:




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