messageboard-2020-05-30-1312.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









11061107110811091110111111121113111411151116111711181119112011211122112311241125 112611271128112911301131113211331134         11351136  113711381139     1140114111421143114411451146                            11471148114911501151115211531154115511561157115811591160116111621163116411651166








123412351236123712381239124012411242124312441245124612471248124912501251125212531254 125512561257125812591260126112621263126412651266126712681269127012711272127312741275








13891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429








4690469146924693469446954696469746984699470047014702470347044705470647074708470947104711      47124713471447154716471747184719472047214722472347244725472647274728472947304731








49844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024








51335134513551365137513851395140514151425143514451455146514751485149515051515152 515351545155 5156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186








524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334  53355336533753385339534053415342534353445345534653475348534953505351535253535354








53975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440








54545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479  54805481548254835484548554865487548854895490549154925493549454955496549754985499








56195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662











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




      files = [full_path]
    else:
      return []

  data = []

  if filenames:
    return files

  for file in files:
    try:
      with open(file, 'rb') as f:
        while True:
          data.append(pickle.load(f))
    except (EOFError, pickle.UnpicklingError):
      pass

  return data



def PickleObjectToFile(data, full_path, date_segmentation, date_suffix=None):
  """Append one pickled flight to the end of binary file.

  Args:
    data: data to pickle
    full_path: name (potentially including path) of the pickled file
    date_segmentation: boolean indicating whether the date string yyyy-mm-dd should be
      prepended to the file name in full_path based on the current date, so that
      pickled files are segmented by date.









  """
  if not date_suffix:


    date_suffix = EpochDisplayTime(time.time(), '%Y-%m-%d-')
  if date_segmentation:
    full_path = PrependFileName(full_path, date_suffix)






  try:
    with open(full_path, 'ab') as f:
      f.write(pickle.dumps(data))

  except IOError:
    Log('Unable to append pickle ' + full_path)






























def UpdateAircraftList(persistent_nearby_aircraft, current_nearby_aircraft, now):
  """Identifies newly seen aircraft and removes aircraft that haven't been seen recently.

  Updates persistent_nearby_aircraft as follows: flights that have been last seen more
  than PERSISTENCE_SECONDS seconds ago are removed; new flights in current_nearby_aircraft
  are added. Also identifies newly-seen aircraft and updates the last-seen timestamp of
  flights that have been seen again.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight number / squawk tuples,
      and the values are the time the flight was last seen.
    current_nearby_aircraft: dictionary where keys are flight numbers / squawk tuples,
      and the values are themselves dictionaries with key-value pairs about that
      flight, with at least one of the kv-pairs being the time the flight was seen.
    now: the timestamp of the flights in the current_nearby_aircraft.

  Returns:
    A list of newly-nearby flight identifiers (i.e.: 2-tuple of flight number / squawk).




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





      if len(newly_nearby_flight_identifiers) > 1:
        newly_nearby_flight_identifiers_str = ', '.join(newly_nearby_flight_identifiers)
        newly_nearby_flight_details_str = '\n'.join(
            [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers])
        Log('Multiple newly-nearby flights: %s\n%s' % (
            newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str))
      flight_identifier = newly_nearby_flight_identifiers[0]

      flight_aware_json = {}
      if SIMULATION:
        json_times = [j[1] for j in FA_JSONS]
        if json_time in json_times:
          flight_aware_json = FA_JSONS[json_times.index(json_time)][0]
      elif flight_identifier[0]:
        flight_number = flight_identifier[0]
        flight_aware_json = GetFlightAwareJson(flight_number)
        if flight_aware_json:
          UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False)
        else:
          Log('No json returned from Flightaware for flight: %s' % flight_number)

          UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True)

      flight_details = {}
      if flight_aware_json:
        flight_details = ParseFlightAwareJson(flight_aware_json)

      if not SIMULATION and log_jsons:
        PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True)

      # Augment FlightAware details with radio / radio-derived details
      flight_details.update(current_nearby_aircraft[flight_identifier])

      # Augment with the past location data; the [1] is because recall that
      # persistent_path[key] is actually a 2-tuple, the first element being
      # the most recent time seen, and the second element being the actual
      # path. But we do not need to keep around the most recent time seen any
      # more.
      flight_details['persistent_path'] = persistent_path[flight_identifier][1]

  return (
      persistent_nearby_aircraft,




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





  Args:
    dump_json: The text representation of the json message from dump1090-mutability
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.

  Returns:
    Return tuple:
    - dictionary of all nearby planes, where keys are flight numbers (i.e.: 'SWA7543'),
      and the value is itself a dictionary of attributes.
    - time stamp in the json file.
    - dictionary of attributes about the radio range
    - persistent dictionary of the track of recent flights, where keys are the flight
      numbers and the value is a tuple, the first element being when the flight was last
      seen in this radio, and the second is a list of dictionaries with past location info
      from the radio where it's been seen, i.e.: d[flight] = (timestamp, [{}, {}, {}])
  """
  parsed = json.loads(dump_json)
  now = parsed['now']
  print(EpochDisplayTime(now))  #TODO
  nearby_aircraft = {}

  # Build dictionary summarizing characteristics of the dump_json itself
  json_desc_dict = DescribeDumpJson(parsed)

  for aircraft in parsed['aircraft']:
    simplified_aircraft = {}

    simplified_aircraft['now'] = now

    # flight_number
    flight_number = aircraft.get('flight')
    if flight_number:
      flight_number = flight_number.strip()

    # squawk
    squawk = aircraft.get('squawk')
    if squawk:
      squawk = squawk.strip()





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




  FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True)

  global ALL_MESSAGE_FILE
  ALL_MESSAGE_FILE = PrependFileName(ALL_MESSAGE_FILE, SIMULATION_PREFIX)
  ClearFile(ALL_MESSAGE_FILE)

  global LOGFILE
  LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX)
  ClearFile(LOGFILE)

  global ROLLING_LOGFILE
  ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX)
  ClearFile(ROLLING_LOGFILE)

  global ROLLING_MESSAGE_FILE
  ROLLING_MESSAGE_FILE = PrependFileName(ROLLING_MESSAGE_FILE, SIMULATION_PREFIX)
  ClearFile(ROLLING_MESSAGE_FILE)

  global PICKLE_FLIGHTS
  PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX)
  ClearFile(PICKLE_FLIGHTS)
  filenames = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=None, filenames=True)






  for file in filenames:
    ClearFile(file)


def SimulationEnd(message_queue, flights):
  """Clears message buffer, exercises histograms, and other misc test & status code.

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





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




      args=(to_remote_q, to_main_q, shutdown[0]))
  servo = ValidateSingleRunning(
      'enable_servos' in configuration,
      arduino.ServoMain, p=servo,
      args=(to_servo_q, to_main_q, shutdown[1]))
  return remote, servo


def ValidateSingleRunning(enabled, start_function, p=None, args=()):
  """Restarts a new instance of multiprocessing process if not running"""
  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))
      else:
        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 = True  # has been set to True  #TODO
      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()





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




  # See https://stackoverflow.com/questions/31826814/curl-post-request-into-pycurl-code
  # Set URL value
  curl.setopt(
      pycurl.URL,
      'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id)
  curl.setopt(pycurl.HTTPHEADER, [
      'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret])
  curl.setopt(pycurl.TIMEOUT_MS, timeout*1000)
  curl.setopt(pycurl.POST, 1)

  curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean

  # preparing body the way pycurl.READDATA wants it
  body_as_dict = {'text': s}
  body_as_json_string = json.dumps(body_as_dict) # dict to json
  body_as_file_object = io.StringIO(body_as_json_string)

  # prepare and send. See also: pycurl.READFUNCTION to pass function instead
  curl.setopt(pycurl.READDATA, body_as_file_object)
  curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string))

  try:
    curl.perform()
  except pycurl.error as e:

    Log('curl.perform() failed with message %s' % e)
    error_code = True
  else:
    # you may want to check HTTP response code, e.g.
    status_code = curl.getinfo(pycurl.RESPONSE_CODE)
    if status_code != 200:
      Log('Server returned HTTP status code %d for message %s' % (status_code, s))
      error_code = True

  curl.close()
  UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, error_code)


def ManageMessageQueue(message_queue, next_message_time, configuration):
  """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.

  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[:]




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






def ResetLogs(config):
  """Clears the non-scrolling logs if reset_logs in config."""
  if 'reset_logs' in config:
    Log('Reset logs')
    for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE):
      if RemoveFile(f):
        open(f, 'a').close()
    config.pop('reset_logs')
    config = BuildSettings(config)
    WriteFile(CONFIG_FILE, config)
  return config


def CheckTemperature():
  """Turn on fan if temperature exceeds threshold."""
  if RASPBERRY_PI:
    temperature = gpiozero.CPUTemperature().temperature
    if temperature > TEMP_FAN_TURN_ON_CELSIUS:
      UpdateStatusLight(GPIO_FAN, True)
    elif temperature < TEMP_FAN_TURN_OFF_CELSIUS:
      UpdateStatusLight(GPIO_FAN, False)


pin_values = {}  # caches last set value
def SetPinMode():
  """Initialize output GPIO pins for output on Raspberry Pi."""
  global pin_values

  if RASPBERRY_PI:
    RPi.GPIO.setmode(RPi.GPIO.BCM)

  pins = (
      GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION,
      GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
      GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2)

  for pin in pins:
    initial_state = pin[5]
    pin_values[pin[0]] = initial_state  # Initialize state of pins
    UpdateDashboard(initial_state, pin)    

    if RASPBERRY_PI:
      RPi.GPIO.setup(pin[0], RPi.GPIO.OUT)
      RPi.GPIO.output(pin[0], pin_values[pin[0]])
    UpdateDashboard(pin_values[pin[0]], pin)

  if RASPBERRY_PI:  # configure soft reset button
    RPi.GPIO.setup(GPIO_SOFT_RESET[0], RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN)
    RPi.GPIO.setup(GPIO_SOFT_RESET[1], RPi.GPIO.OUT)
    RPi.GPIO.output(GPIO_SOFT_RESET[1], True)
    RPi.GPIO.add_event_detect(GPIO_SOFT_RESET[0], RPi.GPIO.RISING)
    RPi.GPIO.add_event_callback(GPIO_SOFT_RESET[0], InterruptRebootFromButton)


def UpdateStatusLight(pin, value):
  """Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value."""
  global pin_values

  if value:
    msg = pin[1]
  else:
    msg = pin[2]
  if RASPBERRY_PI:
    RPi.GPIO.output(pin[0], value)
    if value:
      pin_setting = 'HIGH'
      relay_light_value = 'OFF'
    else:
      pin_setting = 'LOW'
      relay_light_value = 'ON'
    msg += '; RPi GPIO pin %d set to %s; relay light #%d should now be %s' % (
        pin[0], pin_setting, pin[3], relay_light_value)

  if pin_values[pin[0]] != value:
    if VERBOSE:
      Log(msg)  # log
    pin_values[pin[0]] = value  # update cache
    UpdateDashboard(value, pin)


def UpdateDashboard(value, subsystem=0):
  versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO)
  if subsystem:
    subsystem = subsystem[0]
  PickleObjectToFile((time.time(), subsystem, value, versions), PICKLE_DASHBOARD, True)




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


def ConfirmNewFlight(flight, flights):
  """Replaces last-seen flight with new flight if otherwise identical but for identifiers.

  Flights are identified by the radio over time by a tuple of identifiers: flight_number
  and squawk.  Due to unknown communication issues, one or the other may not always
  be transmitted. However, as soon as a new flight is identified that has at least one
  of those identifiers, we report on it and log it to the pickle repository, etc.

  This function checks if the newly identified flight is indeed a duplicate of the
  immediate prior flight by virtue of having the same squawk and/or flight number, and




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




  # There is potential complication in that the last flight and the new flight
  # crossed into a new day, and we are using date segmentation so that the last
  # flight exists in yesterday's file
  max_days = 1
  if not SIMULATION and DisplayTime(flight, '%x') != DisplayTime(last_flight, '%x'):
    max_days = 2
    message += (
        '; in repickling, we crossed days, so pickled flights that might otherwise'
        ' be in %s file are now all located in %s file' % (
            DisplayTime(last_flight, '%x'), DisplayTime(flight, '%x')))

  Log(message)

  args = (PICKLE_FLIGHTS, not SIMULATION, max_days)
  saved_flights = UnpickleObjectFromFile(*args)[:-1]
  files_to_overwrite = UnpickleObjectFromFile(*args, filenames=True)

  for file in files_to_overwrite:
    os.remove(file)
  for f in saved_flights:
    if SPLIT_SIMULATION_FLIGHT_PICKLE:
      PickleObjectToFile(f, PICKLE_FLIGHTS, True, date_suffix=DisplayTime(f, '%Y-%m-%d-'))
    else:
      PickleObjectToFile(f, PICKLE_FLIGHTS, not SIMULATION)

  return False


def HeartbeatRestart():
  if SIMULATION:
    return 0
  UpdateDashboard(True)  # Indicates that this wasn't running a moment before, ...
  UpdateDashboard(False)  # ... and now it is running!
  return time.time()


def Heartbeat(last_heartbeat_time):
  if SIMULATION:
    return last_heartbeat_time
  now = time.time()
  if now - last_heartbeat_time > HEARTBEAT_SECONDS:
    UpdateDashboard(False)
    last_heartbeat_time = now
  return last_heartbeat_time




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




    last_modified_suffix = EpochDisplayTime(epoch, format_string='-%Y-%m-%d-%H%M')
    version_name = python_prefix + last_modified_suffix + file_extension
    version_path = os.path.join(VERSION_REPOSITORY, version_name)

    shutil.copyfile(live_path, version_path)
    return version_name

  VERSION_MESSAGEBOARD = MakeCopy('messageboard')
  VERSION_ARDUINO = MakeCopy('arduino')


def main():
  """Traffic cop between incoming radio flight messages, configuration, and messageboard.

  This is the main logic, checking for new flights, augmenting the radio signal with
  additional web-scraped data, and generating messages in a form presentable to the
  messageboard.
  """
  RemoveFile(LOGFILE_LOCK)
  VersionControl()
  last_heartbeat_time = HeartbeatRestart()

  # Since this clears log files, it should occur first before we start logging
  if '-s' in sys.argv:
    global SIMULATION_COUNTER
    SimulationSetup()



  # This flag slows down simulation time around a flight, great for debugging the arduinos
  simulation_slowdown = bool('-f' in sys.argv)

  # Redirect any errors to a log file instead of the screen, and add a datestamp
  if not SIMULATION:
    sys.stderr = open(STDERR_FILE, 'a')
    Log('', STDERR_FILE)

  Log('Starting up process %d' % os.getpid())
  already_running_ids = FindRunningParents()
  if already_running_ids:
    for pid in already_running_ids:
      Log('Sending termination signal to %d' % pid)
      os.kill(pid, signal.SIGTERM)

  SetPinMode()

  configuration = ReadAndParseSettings(CONFIG_FILE)
  startup_time = time.time()




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




          # 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_INTERESTING, 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'] = []

        if SPLIT_SIMULATION_FLIGHT_PICKLE:
          PickleObjectToFile(flight, PICKLE_FLIGHTS, True, date_suffix=DisplayTime(flight, '%Y-%m-%d-'))
        else:
          PickleObjectToFile(flight, PICKLE_FLIGHTS, not SIMULATION)

      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)

    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)





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





01234567890123456789012345678901234567890123456789012345678901234567890123456789









1106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211








1279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321








14351436143714381439144014411442144314441445144614471448144914501451145214531454 14551456145714581459146014611462146314641465146614671468146914701471147214731474








47354736473747384739474047414742474347444745474647474748474947504751475247534754 475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781








50345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074








51835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238








5300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408








54515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494








55085509551055115512551355145515551655175518551955205521552255235524552555265527 552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554








56745675567656775678567956805681568256835684568556865687568856895690569156925693 5694  56955696569756985699570057015702570357045705570657075708570957105711571257135714











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




      files = [full_path]
    else:
      return []

  data = []

  if filenames:
    return files

  for file in files:
    try:
      with open(file, 'rb') as f:
        while True:
          data.append(pickle.load(f))
    except (EOFError, pickle.UnpicklingError):
      pass

  return data


cached_object_count = {}
def PickleObjectToFile(data, full_path, date_segmentation, timestamp=None, verify=False):
  """Append one pickled flight to the end of binary file.

  Args:
    data: data to pickle
    full_path: name (potentially including path) of the pickled file
    date_segmentation: boolean indicating whether the date string yyyy-mm-dd should be
      prepended to the file name in full_path based on the current date, so that
      pickled files are segmented by date.
    timestamp: if date_segmentation is True, this is used rather than system time
      to generate the file name.
    verify: boolean indicating if we should verify that the pickled file object count
      increments by one, rewriting entire pickle file if it doesn't. Note that since
      this requires reading the entire pickle file and unpickling, it should only be
      done for small files / objects.

  Returns:
    Name of file to which the data was pickled if successful; None if failed.
  """
  global cached_object_count
  if not timestamp:
    timestamp = time.time()
  date_suffix = EpochDisplayTime(timestamp, '%Y-%m-%d-')
  if date_segmentation:
    full_path = PrependFileName(full_path, date_suffix)

  if full_path not in cached_object_count:
    cached_object_count[full_path] = len(UnpickleObjectFromFile(full_path, False))
  if not os.path.exists(full_path):  # Another method may delete the file
    cached_object_count[full_path] = 0

  try:
    with open(full_path, 'ab') as f:
      f.write(pickle.dumps(data))

  except IOError:
    Log('Unable to append pickle ' + full_path)
    return None

  if verify:
    # file object count should now be one more; if it isn't, the file is corrupted, and
    # rather than continue writing to a corrupted pickle file, we should fix it so we
    # don't lose too much data
    pickled_data = UnpickleObjectFromFile(full_path, False)
    cached_count = cached_object_count[full_path]
    if len(pickled_data) == cached_count + 1:
      cached_object_count[full_path] = cached_count + 1
    else:
      tmp_file_name = full_path + '.tmp'
      try:
        with open(tmp_file_name, 'ab') as f:
          for d in pickled_data:  # rewrite the old data that was retained
            f.write(pickle.dumps(d))
          f.write(pickle.dumps(data))  # new data
      except IOError:
        Log('Unable to append pickle %s in verify step; left tmp file as-is' %
            tmp_file_name)
        return None
      shutil.move(tmp_file_name, full_path)
      cached_object_count[full_path] = len(pickled_data) + 1
      Log('Re-pickled %s: after writing %s, expected len %d to increment, '
          'but it did not; after repickling (and adding the new data), new length = %d' % (
              full_path, data, cached_count, cached_object_count[full_path]))

  return full_path


def UpdateAircraftList(persistent_nearby_aircraft, current_nearby_aircraft, now):
  """Identifies newly seen aircraft and removes aircraft that haven't been seen recently.

  Updates persistent_nearby_aircraft as follows: flights that have been last seen more
  than PERSISTENCE_SECONDS seconds ago are removed; new flights in current_nearby_aircraft
  are added. Also identifies newly-seen aircraft and updates the last-seen timestamp of
  flights that have been seen again.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight number / squawk tuples,
      and the values are the time the flight was last seen.
    current_nearby_aircraft: dictionary where keys are flight numbers / squawk tuples,
      and the values are themselves dictionaries with key-value pairs about that
      flight, with at least one of the kv-pairs being the time the flight was seen.
    now: the timestamp of the flights in the current_nearby_aircraft.

  Returns:
    A list of newly-nearby flight identifiers (i.e.: 2-tuple of flight number / squawk).




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





      if len(newly_nearby_flight_identifiers) > 1:
        newly_nearby_flight_identifiers_str = ', '.join(newly_nearby_flight_identifiers)
        newly_nearby_flight_details_str = '\n'.join(
            [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers])
        Log('Multiple newly-nearby flights: %s\n%s' % (
            newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str))
      flight_identifier = newly_nearby_flight_identifiers[0]

      flight_aware_json = {}
      if SIMULATION:
        json_times = [j[1] for j in FA_JSONS]
        if json_time in json_times:
          flight_aware_json = FA_JSONS[json_times.index(json_time)][0]
      elif flight_identifier[0]:
        flight_number = flight_identifier[0]
        flight_aware_json = GetFlightAwareJson(flight_number)
        if flight_aware_json:
          UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False)
        else:
          failure_message = 'No json from Flightaware for flight: %s' % flight_number
          Log(failure_message)
          UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True, failure_message)

      flight_details = {}
      if flight_aware_json:
        flight_details = ParseFlightAwareJson(flight_aware_json)

      if not SIMULATION and log_jsons:
        PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True)

      # Augment FlightAware details with radio / radio-derived details
      flight_details.update(current_nearby_aircraft[flight_identifier])

      # Augment with the past location data; the [1] is because recall that
      # persistent_path[key] is actually a 2-tuple, the first element being
      # the most recent time seen, and the second element being the actual
      # path. But we do not need to keep around the most recent time seen any
      # more.
      flight_details['persistent_path'] = persistent_path[flight_identifier][1]

  return (
      persistent_nearby_aircraft,




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





  Args:
    dump_json: The text representation of the json message from dump1090-mutability
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.

  Returns:
    Return tuple:
    - dictionary of all nearby planes, where keys are flight numbers (i.e.: 'SWA7543'),
      and the value is itself a dictionary of attributes.
    - time stamp in the json file.
    - dictionary of attributes about the radio range
    - persistent dictionary of the track of recent flights, where keys are the flight
      numbers and the value is a tuple, the first element being when the flight was last
      seen in this radio, and the second is a list of dictionaries with past location info
      from the radio where it's been seen, i.e.: d[flight] = (timestamp, [{}, {}, {}])
  """
  parsed = json.loads(dump_json)
  now = parsed['now']

  nearby_aircraft = {}

  # Build dictionary summarizing characteristics of the dump_json itself
  json_desc_dict = DescribeDumpJson(parsed)

  for aircraft in parsed['aircraft']:
    simplified_aircraft = {}

    simplified_aircraft['now'] = now

    # flight_number
    flight_number = aircraft.get('flight')
    if flight_number:
      flight_number = flight_number.strip()

    # squawk
    squawk = aircraft.get('squawk')
    if squawk:
      squawk = squawk.strip()





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




  FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True)

  global ALL_MESSAGE_FILE
  ALL_MESSAGE_FILE = PrependFileName(ALL_MESSAGE_FILE, SIMULATION_PREFIX)
  ClearFile(ALL_MESSAGE_FILE)

  global LOGFILE
  LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX)
  ClearFile(LOGFILE)

  global ROLLING_LOGFILE
  ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX)
  ClearFile(ROLLING_LOGFILE)

  global ROLLING_MESSAGE_FILE
  ROLLING_MESSAGE_FILE = PrependFileName(ROLLING_MESSAGE_FILE, SIMULATION_PREFIX)
  ClearFile(ROLLING_MESSAGE_FILE)

  global PICKLE_FLIGHTS
  PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX)

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

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


def SimulationEnd(message_queue, flights):
  """Clears message buffer, exercises histograms, and other misc test & status code.

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





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




      args=(to_remote_q, to_main_q, shutdown[0]))
  servo = ValidateSingleRunning(
      'enable_servos' in configuration,
      arduino.ServoMain, p=servo,
      args=(to_servo_q, to_main_q, shutdown[1]))
  return remote, servo


def ValidateSingleRunning(enabled, start_function, p=None, args=()):
  """Restarts a new instance of multiprocessing process if not running"""
  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 = True  # has been set to True  #TODO
      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()





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




  # See https://stackoverflow.com/questions/31826814/curl-post-request-into-pycurl-code
  # Set URL value
  curl.setopt(
      pycurl.URL,
      'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id)
  curl.setopt(pycurl.HTTPHEADER, [
      'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret])
  curl.setopt(pycurl.TIMEOUT_MS, timeout*1000)
  curl.setopt(pycurl.POST, 1)

  curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean

  # preparing body the way pycurl.READDATA wants it
  body_as_dict = {'text': s}
  body_as_json_string = json.dumps(body_as_dict) # dict to json
  body_as_file_object = io.StringIO(body_as_json_string)

  # prepare and send. See also: pycurl.READFUNCTION to pass function instead
  curl.setopt(pycurl.READDATA, body_as_file_object)
  curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string))
  failure_message = ''
  try:
    curl.perform()
  except pycurl.error as e:
    failure_message = 'curl.perform() failed with message %s' % e
    Log('curl.perform() failed with message %s' % e)
    error_code = True
  else:
    # you may want to check HTTP response code, e.g.
    status_code = curl.getinfo(pycurl.RESPONSE_CODE)
    if status_code != 200:
      Log('Server returned HTTP status code %d for message %s' % (status_code, s))
      error_code = True

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


def ManageMessageQueue(message_queue, next_message_time, configuration):
  """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.

  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[:]




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






def ResetLogs(config):
  """Clears the non-scrolling logs if reset_logs in config."""
  if 'reset_logs' in config:
    Log('Reset logs')
    for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE):
      if RemoveFile(f):
        open(f, 'a').close()
    config.pop('reset_logs')
    config = BuildSettings(config)
    WriteFile(CONFIG_FILE, config)
  return config


def CheckTemperature():
  """Turn on fan if temperature exceeds threshold."""
  if RASPBERRY_PI:
    temperature = gpiozero.CPUTemperature().temperature
    if temperature > TEMP_FAN_TURN_ON_CELSIUS:
      UpdateStatusLight(GPIO_FAN, True, 'Temperature: %.1f' % temperature)
    elif temperature < TEMP_FAN_TURN_OFF_CELSIUS:
      UpdateStatusLight(GPIO_FAN, False)


pin_values = {}  # caches last set value
def SetPinMode():
  """Initialize output GPIO pins for output on Raspberry Pi."""
  global pin_values

  if RASPBERRY_PI:
    RPi.GPIO.setmode(RPi.GPIO.BCM)

  pins = (
      GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION,
      GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
      GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2)

  for pin in pins:
    initial_state = pin[5]
    pin_values[pin[0]] = initial_state  # Initialize state of pins
    UpdateDashboard(initial_state, pin)

    if RASPBERRY_PI:
      RPi.GPIO.setup(pin[0], RPi.GPIO.OUT)
      RPi.GPIO.output(pin[0], pin_values[pin[0]])
    UpdateDashboard(pin_values[pin[0]], pin)

  if RASPBERRY_PI:  # configure soft reset button
    RPi.GPIO.setup(GPIO_SOFT_RESET[0], RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN)
    RPi.GPIO.setup(GPIO_SOFT_RESET[1], RPi.GPIO.OUT)
    RPi.GPIO.output(GPIO_SOFT_RESET[1], True)
    RPi.GPIO.add_event_detect(GPIO_SOFT_RESET[0], RPi.GPIO.RISING)
    RPi.GPIO.add_event_callback(GPIO_SOFT_RESET[0], InterruptRebootFromButton)


def UpdateStatusLight(pin, value, failure_message=''):
  """Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value."""
  global pin_values

  if value:
    msg = pin[1]
  else:
    msg = pin[2]
  if RASPBERRY_PI:
    RPi.GPIO.output(pin[0], value)
    if value:
      pin_setting = 'HIGH'
      relay_light_value = 'OFF'
    else:
      pin_setting = 'LOW'
      relay_light_value = 'ON'
    msg += '; RPi GPIO pin %d set to %s; relay light #%d should now be %s' % (
        pin[0], pin_setting, pin[3], relay_light_value)

  if pin_values[pin[0]] != value:
    if VERBOSE:
      Log(msg)  # log
    pin_values[pin[0]] = value  # update cache
    UpdateDashboard(value, subsystem=pin, failure_message=failure_message)


def UpdateDashboard(value, subsystem=0, failure_message=''):
  versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO)
  if subsystem:
    subsystem = subsystem[0]
  PickleObjectToFile(
      (time.time(), subsystem, value, versions, failure_message),
      PICKLE_DASHBOARD, True)


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


def ConfirmNewFlight(flight, flights):
  """Replaces last-seen flight with new flight if otherwise identical but for identifiers.

  Flights are identified by the radio over time by a tuple of identifiers: flight_number
  and squawk.  Due to unknown communication issues, one or the other may not always
  be transmitted. However, as soon as a new flight is identified that has at least one
  of those identifiers, we report on it and log it to the pickle repository, etc.

  This function checks if the newly identified flight is indeed a duplicate of the
  immediate prior flight by virtue of having the same squawk and/or flight number, and




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




  # There is potential complication in that the last flight and the new flight
  # crossed into a new day, and we are using date segmentation so that the last
  # flight exists in yesterday's file
  max_days = 1
  if not SIMULATION and DisplayTime(flight, '%x') != DisplayTime(last_flight, '%x'):
    max_days = 2
    message += (
        '; in repickling, we crossed days, so pickled flights that might otherwise'
        ' be in %s file are now all located in %s file' % (
            DisplayTime(last_flight, '%x'), DisplayTime(flight, '%x')))

  Log(message)

  args = (PICKLE_FLIGHTS, not SIMULATION, max_days)
  saved_flights = UnpickleObjectFromFile(*args)[:-1]
  files_to_overwrite = UnpickleObjectFromFile(*args, filenames=True)

  for file in files_to_overwrite:
    os.remove(file)
  for f in saved_flights:
    # we would like to use verify=True, but that's too slow without further optimizing the
    # verification step for a loop of data
    PickleObjectToFile(
        f, PICKLE_FLIGHTS, True, timestamp=f['now'], verify=False)

  return False


def HeartbeatRestart():
  if SIMULATION:
    return 0
  UpdateDashboard(True)  # Indicates that this wasn't running a moment before, ...
  UpdateDashboard(False)  # ... and now it is running!
  return time.time()


def Heartbeat(last_heartbeat_time):
  if SIMULATION:
    return last_heartbeat_time
  now = time.time()
  if now - last_heartbeat_time > HEARTBEAT_SECONDS:
    UpdateDashboard(False)
    last_heartbeat_time = now
  return last_heartbeat_time




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




    last_modified_suffix = EpochDisplayTime(epoch, format_string='-%Y-%m-%d-%H%M')
    version_name = python_prefix + last_modified_suffix + file_extension
    version_path = os.path.join(VERSION_REPOSITORY, version_name)

    shutil.copyfile(live_path, version_path)
    return version_name

  VERSION_MESSAGEBOARD = MakeCopy('messageboard')
  VERSION_ARDUINO = MakeCopy('arduino')


def main():
  """Traffic cop between incoming radio flight messages, configuration, and messageboard.

  This is the main logic, checking for new flights, augmenting the radio signal with
  additional web-scraped data, and generating messages in a form presentable to the
  messageboard.
  """
  RemoveFile(LOGFILE_LOCK)
  VersionControl()


  # Since this clears log files, it should occur first before we start logging
  if '-s' in sys.argv:
    global SIMULATION_COUNTER
    SimulationSetup()

  last_heartbeat_time = HeartbeatRestart()

  # This flag slows down simulation time around a flight, great for debugging the arduinos
  simulation_slowdown = bool('-f' in sys.argv)

  # Redirect any errors to a log file instead of the screen, and add a datestamp
  if not SIMULATION:
    sys.stderr = open(STDERR_FILE, 'a')
    Log('', STDERR_FILE)

  Log('Starting up process %d' % os.getpid())
  already_running_ids = FindRunningParents()
  if already_running_ids:
    for pid in already_running_ids:
      Log('Sending termination signal to %d' % pid)
      os.kill(pid, signal.SIGTERM)

  SetPinMode()

  configuration = ReadAndParseSettings(CONFIG_FILE)
  startup_time = time.time()




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




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

    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)





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