messageboard-2020-05-21-1953.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









19202122232425262728293031323334353637383940414243444546 4748495051525354555657585960616263646566








152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183                              184185186187188189190191192193194195196197198199200201202203








226227228229230231232233234235236237238239240241242243244245  246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 284285286 287288289290291292293294295296297 298299300301302303304305306 307308309310311312    313314315316317318319320321322323324325326327328329330331332333   334335336337338339340341342343344345346347348349350351352353








945946947948949950951952953954955956957958959960961962963964        965966967968969970971972973974975976977978979980981982983984








11341135113611371138113911401141114211431144114511461147114811491150115111521153 1154115511561157115811591160116111621163116411651166116711681169117011711172117311741175








126812691270127112721273127412751276127712781279128012811282128312841285128612871288   128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339








4496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550         4551  455245534554455545564557 45584559456045614562456345644565           456645674568456945704571457245734574               45754576 4577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601 46024603  460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678    4679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703








4711471247134714471547164717471847194720472147224723472447254726472747284729473047314732     4733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757








48114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851








48704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922








494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019    5020502150225023         5024502550265027      50285029503050315032     5033 50345035 50365037   5038503950405041        504250435044504550465047504850495050  5051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093








51565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196








5203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266



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




import bs4
import dateutil.relativedelta
import filelock
import numpy
import matplotlib
import matplotlib.pyplot
import psutil
import pycurl
import pytz
import requests
import tzlocal
import unidecode

# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'

import arduino  # module expects paths to be set before import  # pylint: disable=C0413

RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'
if RASPBERRY_PI:
  import gpiozero  # pylint: disable=E0401
  import RPi.GPIO  # pylint: disable=E0401

SHUTDOWN_SIGNAL = False


SIMULATION = False
SIMULATION_COUNTER = 0
SIMULATION_PREFIX = 'SIM_'
PICKLE_DUMP_JSON_FILE = 'pickle/dump_json.pk'
PICKLE_FA_JSON_FILE = 'pickle/fa_json.pk'
DUMP_JSONS = None  # loaded only if in simulation mode
FA_JSONS = None  # loaded only if in simulation mode

HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29  #altitude in meters
RADIUS = 6371.0e3  # radius of earth in meters

FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444

MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed




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




FLAG_INSIGHT_GROUNDSPEED = 3
FLAG_INSIGHT_ALTITUDE = 4
FLAG_INSIGHT_VERTRATE = 5
FLAG_INSIGHT_FIRST_DEST = 6
FLAG_INSIGHT_FIRST_ORIGIN = 7
FLAG_INSIGHT_FIRST_AIRLINE = 8
FLAG_INSIGHT_FIRST_AIRCRAFT = 9
FLAG_INSIGHT_LONGEST_DELAY = 10
FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11
FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12
FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13
FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14
FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15
FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16
FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17
FLAG_INSIGHT_HOUR_DELAY_TIME = 18
FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19
FLAG_INSIGHT_DATE_DELAY_TIME = 20
INSIGHT_TYPES = 21

# GPIO connections - value is the RPi pin number for the error light
GPIO_ERROR_VESTABOARD_CONNECTION = 22
GPIO_ERROR_FLIGHT_AWARE_CONNECTION = 23
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = 24
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = 25
GPIO_ERROR_BATTERY_CHARGE = 26
GPIO_FAN = 27
GPIO_UNUSED_1 = 5  # for future expansion
GPIO_UNUSED_2 = 6  # for future expansion

TEMP_FAN_TURN_ON_CELSIUS = 65
TEMP_FAN_TURN_OFF_CELSIUS = 55































#if running on raspberry, then need to prepend path to file names
if RASPBERRY_PI:
  PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS
  LOGFILE = MESSAGEBOARD_PATH + LOGFILE
  PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
  PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE

  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

  HISTOGRAM_IMAGE_PREFIX = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX
  HISTOGRAM_EMPTY_IMAGE_FILE = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)




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




SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
MINUTES_IN_DAY = MINUTES_IN_HOUR * HOURS_IN_DAY
SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY

# Units confirmed here:
# www.adsbexchange.com/forum/threads/units-in-the-dump1090-json-file.630617/#post-639541
CLIMB_RATE_UNITS = 'fpm'
#speed units from tracker are knots, based on dump-1090/track.c
#https://github.com/SDRplay/dump1090/blob/master/track.c
SPEED_UNITS = 'kn'
DISTANCE_UNITS = 'ft'  # altitude

# For displaying histograms
# If a key is not present, how should it be displayed in histograms?
KEY_NOT_PRESENT_STRING = 'Unknown'
OTHER_STRING = 'Other' # What key strings should be listed last in sequence?
# What key strings should be listed last in sequence?
SORT_AT_END_STRINGS = [OTHER_STRING, KEY_NOT_PRESENT_STRING]
# What is the sorted sequence of keys for days of week?
DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']



aircraft_length = {} # in meters
aircraft_length['Airbus A220-100 (twin-jet)'] = 35
aircraft_length['Airbus A300F4-600 (twin-jet)'] = 54.08
aircraft_length['Airbus A319 (twin-jet)'] = 33.84
aircraft_length['Airbus A320 (twin-jet)'] = 37.57
aircraft_length['Airbus A320neo (twin-jet)'] = 37.57
aircraft_length['Airbus A321 (twin-jet)'] = 44.51
aircraft_length['Airbus A321neo (twin-jet)'] = 44.51
aircraft_length['Airbus A330-200 (twin-jet)'] = 58.82
aircraft_length['Airbus A330-300 (twin-jet)'] = 63.67
aircraft_length['Airbus A340-300 (quad-jet)'] = 63.69
aircraft_length['Airbus A350-1000 (twin-jet)'] = 73.79
aircraft_length['Airbus A350-900 (twin-jet)'] = 66.8
aircraft_length['Airbus A380-800 (quad-jet)'] = 72.72
aircraft_length['Boeing 737-400 (twin-jet)'] = 36.4
aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92
aircraft_length['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2

aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46

aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Citation X (twin-jet)'] = 22.04
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
aircraft_length['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82

aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4


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

  Args:
    message: string message to write
    file: string representing file name and, if needed, path to the file to write to

  """
  # can't define as a default parameter because LOGFILE name is potentially
  # modified based on SIMULATION flag
  if not file:
    file = LOGFILE





  if file == LOGFILE:
    lock = filelock.FileLock(LOGFILE_LOCK)
    lock.acquire()

  try:
    with open(file, 'a') as f:
      # by excluding the timestamp, file diffs become easy between runs
      if not SIMULATION or file == LOGFILE:
        f.write('='*80+'\n')
        f.write(str(datetime.datetime.now(TZ))+'\n')
        f.write('\n')
      f.write(str(message)+'\n')
  except IOError:
    Log('Unable to append to ' + file)

  if file == LOGFILE:
    lock.release()

  existing_log_lines = ReadFile(LOGFILE).splitlines()
  with open(ROLLING_LOGFILE, 'w') as f:
    f.write('\n'.join(existing_log_lines[-1000:]))





def MaintainRollingWebLog(message, max_count, filename=None):
  """Maintains a rolling text file of at most max_count printed messages.

  Newest data at top and oldest data at the end, of at most max_count messages,
  where the delimiter between each message is identified by a special fixed string.

  Args:
    message: text message to prepend to the file.
    max_count: maximum number of messages to keep in the file; the max_count+1st message
      is deleted.
    filename: the file to update.
  """
  # can't define as a default parameter because ROLLING_MESSAGE_FILE name is potentially
  # modified based on SIMULATION flag
  if not filename:
    filename = ROLLING_MESSAGE_FILE
  rolling_log_header = '='*(SPLITFLAP_CHARS_PER_LINE + 2)
  existing_file = ReadFile(filename)




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




    available or if delimiters missing.
  """
  settings_dict = {}
  for setting in settings.split(';'):
    if '=' in setting:
      kv_list = setting.split('=')
      k = kv_list[0]
      v = kv_list[1]
      if v.isdigit():
        v = int(v)
      else:
        try:
          v = float(v)
        except ValueError:
          pass
      settings_dict[k] = v

  return settings_dict










def WriteFile(filename, text, log_exception=False):
  """Writes the text to the file, returning boolean indicating success.

  Args:
    filename: string of the filename to open, potentially also including the full path.
    text: the text to write
    log_exception: boolean indicating whether to log an exception if file not found.

  Returns:
    Boolean indicating whether the write was successful.
  """
  try:
    with open(filename, 'w') as content_file:
      content_file.write(text)
  except IOError:
    if log_exception:
      Log('Unable to write to '+filename)
    return False
  return True





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




    newly_nearby_flight_numbers = UpdateAircraftList(
        persistent_nearby_aircraft, current_nearby_aircraft, now)

    if newly_nearby_flight_numbers:

      if len(newly_nearby_flight_numbers) > 1:
        newly_nearby_flight_numbers_str = ', '.join(newly_nearby_flight_numbers)
        newly_nearby_flight_details_str = '\n'.join(
            [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_numbers])
        Log('Multiple newly-nearby flights: %s\n%s' % (
            newly_nearby_flight_numbers_str, newly_nearby_flight_details_str))
      flight_number = newly_nearby_flight_numbers[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_number:
        flight_aware_json = GetFlightAwareJson(flight_number)

        UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False)
        if not flight_aware_json:
          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_number])

      # Augment with the past location data
      flight_details['persistent_path'] = persistent_path[flight_number][1]

  return (
      persistent_nearby_aircraft,
      flight_details,
      now,




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





        if aircraft.get('squawk') is not None:
          simplified_aircraft['squawk'] = aircraft.get('squawk')

        track = aircraft.get('track')
        if isinstance(track, numbers.Number):
          min_meters = MinMetersToHome((lat, lon), track)
          simplified_aircraft['track'] = track
          simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER

        if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
          nearby_aircraft[flight_number] = simplified_aircraft

        # keep all that track info - once we start reporting on a nearby flight, it will
        # become part of the flight's persistent record. Also, note that as we are
        # building a list of tracks for each flight, and we are later assigning the
        # flight dictionary to point to the list, we just simply need to continue
        # updating this list to keep the dictionary up to date (i.e.: we don't need
        # to directly touch the flights dictionary in main).
        (last_seen, current_path) = persistent_path.get(flight_number, (None, []))
        if simplified_aircraft not in current_path:



          current_path.append(simplified_aircraft)
        persistent_path[flight_number] = (now, current_path)

  # if the flight was last seen too far in the past, remove the track info
  for flight_number in list(persistent_path.keys()):
    (last_seen, current_path) = persistent_path[flight_number]
    if last_seen < now - PERSISTENCE_SECONDS:
      persistent_path.pop(flight_number)

  return (nearby_aircraft, now, json_desc_dict, persistent_path)


def GetFlightAwareJson(flight_number):
  """Scrapes the text json message from FlightAware for a given flight number.

  Given a flight number, loads the corresponding FlightAware webpage for that flight and
  extracts the relevant script that contains all the flight details from that page.

  Args:
    flight_number: text flight number (i.e.: SWA1234)

  Returns:
    Text representation of the json message from FlightAware.
  """
  url = 'https://flightaware.com/live/flight/' + flight_number
  try:
    response = requests.get(url)
  except requests.exceptions.RequestException as e:
    Log('Unable to query FA for URL due to %s: %s' % (e, url))
    return ''
  soup = bs4.BeautifulSoup(response.text, "html.parser")
  l = soup.find_all('script')
  flight_script = None
  for script in l:
    if "trackpollBootstrap" in str(script):
      flight_script = str(script)
      break
  if not flight_script:
    Log('Unable to find trackpollBootstrap script in page: ' + response.text)
    return ''
  first_open_curly_brace = flight_script.find('{')
  last_close_curly_brace = flight_script.rfind('}')
  flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1]
  return flight_json


def Unidecode(s):
  """Convert a special unicode characters to closest ASCII representation."""
  if s is not None:
    s = unidecode.unidecode(s)
  return s




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




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

    while message_queue:
      ManageMessageQueue(message_queue, 0, {'setting_delay': 0})
    SaveFlightsByAltitudeDistanceCSV(flights)
    SaveFlightsToCSV(flights)

  # repickle to a new .pk with full track info
  file_parts = PICKLE_FLIGHTS.split('.')
  new_pickle_file = '.'.join([file_parts[0] + '_full_path', file_parts[1]])
  if os.path.exists(new_pickle_file):
    os.remove(new_pickle_file)
  for flight in flights:
    PickleObjectToFile(flight, new_pickle_file, False)

  print('Simulation complete after %s dump json messages processed' % len(DUMP_JSONS))


def SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft):
  """Slows down simulations when a reported-upon flight is nearby."""
  if flights and flights[-1].get('flight_number') in persistent_nearby_aircraft:
    time.sleep(arduino.WRITE_DELAY_TIME)


def DumpJsonChanges():
  """Identifies if sequential dump json files changes, for simulation optimization.

  If we are logging the radio output faster than it is updating, then there will be
  sequential log files in the json list that are identical; we only need to process the
  first of these, and can ignore subsequent ones, without any change of output in the
  simulation results. This function identifies whether the current active json changed
  from the prior one.

  Returns:
    Boolean - True if different (and processing needed), False if identical
  """
  if SIMULATION_COUNTER == 0:
    return True
  (this_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER]
  (last_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER - 1]
  return this_json != last_json


def CheckRebootNeeded(startup_time, message_queue, json_desc_dict):
  """Reboot if running for over 24 hours and all is quiet, or if its just been too long."""









  reboot = False


  running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
  if (
      running_hours >= HOURS_IN_DAY and
      not message_queue and
      not json_desc_dict.get('radio_range_flights')):
    reboot = True

  if (
      running_hours > HOURS_IN_DAY * 1.5 and
      not message_queue and
      int(EpochDisplayTime(time.time(), '%-H')) >= 3):
    reboot = True

  if reboot:
    Log('Reboot needed after running for %.2f hours' % running_hours)











    global SHUTDOWN_SIGNAL
    SHUTDOWN_SIGNAL = True

  return reboot


def RequestGracefulShutdown(signalNumber, unused_frame):
  """Sets flag so that the main loop will terminate when it completes the iteration.
















  The function signature is defined by the python language - i.e.: these two variables
  are passed automatically for registered signals.

  """
  global SHUTDOWN_SIGNAL
  SHUTDOWN_SIGNAL = True
  Log('%d received termination signal %d (%s)' % (
      os.getpid(), signalNumber,
      signal.Signals(signalNumber).name))  # pylint: disable=E1101


def PerformGracefulShutdown(queues, shutdown, reboot=False):
  """Complete the graceful shutdown process by cleaning up.

  Args:
    queues: iterable of queues shared with child processes to be closed
    shutdown: tuple of shared flags with child processes to initiate shutdown in children
    reboot: boolean indicating whether we should trigger a reboot
  """
  reboot_msg = ''
  if reboot:
    reboot_msg = ' and rebooting'
  Log('Shutting down self (%d)%s' % (os.getpid(), reboot_msg))

  for q in queues:
    q.close()
  for v in shutdown:  # send the shutdown signal to child processes
    v.value = 1

  RPi.GPIO.cleanup()
  if reboot:


    os.system('sudo reboot')
  sys.exit()


def FindRunningParents():
  """Returns list of proc ids of processes with identically-named python file running.

  In case there are multiple children processes spawned with the same name, such as via
  multiprocessing, this will only return the parent id (since a killed child process
  will likely just be respawned).
  """
  this_process_id = os.getpid()
  this_process_name = os.path.basename(sys.argv[0])
  pids = []
  pid_pairs = []
  for proc in psutil.process_iter():
    try:
      # Check if process name contains this_process_name.
      commands = proc.as_dict(attrs=['cmdline', 'pid', 'ppid'])
      if commands['cmdline']:
        command_running = any(
            [this_process_name in s for s in commands['cmdline']])
        if command_running:
          pids.append(commands['pid'])
          pid_pairs.append((commands['pid'], commands['ppid']))
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
      pass

  # Exclude those pids that have a parent that is also a pid
  final_pids = []
  for pid_pair in pid_pairs:
    if pid_pair[1] not in pids:
      final_pids.append(pid_pair[0])
  # Exclude this pid
  final_pids.pop(final_pids.index(this_process_id))

  return sorted(final_pids)


def WaitUntilKillComplete(already_running_ids, max_seconds=10):
  """Prevents main loop from starting until other instance, if any, completes shutdown.

  A termination command send to any other identically-named process may take a few
  seconds to complete because that other process is allowed to finish the current iteration
  in the main loop. Typically, that iteration in the other process will complete before
  this process finishes the initialization and starts. But in limited scenarios, that might
  not happen, such as if the other process is in the middle of generating a lot of
  histogram images, or if this process does not have much data to load.

  This function ensures that this process does not start the main loop until the other
  process terminates. If it detects that the other process is still running, it waits
  for up to max_seconds. If the other process does not terminate before that time limit,
  then this process terminates.
  """
  still_running_ids = FindRunningParents()
  if not still_running_ids:
    return
  # still_running_ids should at least be a subset of already_running_ids
  new_processes = sorted(list(set(still_running_ids).difference(set(already_running_ids))))
  if new_processes:
    # uh-oh! at least one new started up in the interim? exit!
    Log('Kill signal sent to %s from this process %s, but it seems like there is '
        'at least one new process running, %s!' % (
            str(already_running_ids), str(os.getpid()), str(new_processes)))
    sys.exit()
  # phew - they're a subset; so they probably got the signal; just wait a few secs
  elif still_running_ids:
    n = 0
    running_parents = FindRunningParents()
    while running_parents:
      if n == max_seconds:
        Log('Kill signal sent from this process %d to %s, but %s still '
            'running after waiting cume %d seconds; exiting anyways' % (
                os.getpid(), str(already_running_ids), str(running_parents), n+1))
        sys.exit()




      n += 1
      time.sleep(1)
      Log('Kill signal sent from this process %d to %s, but %s still '
          'running after waiting cume %d seconds' % (
              os.getpid(), str(already_running_ids), str(running_parents), n+1))
      running_parents = FindRunningParents()


def InitArduinos(configuration):
  """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)

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


def RefreshArduinos(
    remote, servo,
    to_remote_q, to_servo_q, to_main_q, shutdown,




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





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."""
  remote = ValidateSingleRunning(
      'enable_remote' in configuration,
      arduino.RemoteMain, p=remote,
      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 shut this down
        args[2].value = 1  # signal a shutdown on the shared state arg





      else:
        return None

    elif p is None or not p.is_alive():
      Log('Process (%s) for %s is not alive; 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
      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 = dict(flights[-1])

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






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




      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_INTERESTING]
        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_settings':
      WriteFile(CONFIG_FILE, *args)

    else:
      Log('Improper command from arduinos: %s / %s' % (command, args))

  return message_queue, next_message_time


def PublishMessage(
    s,
    subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
    key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
    secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
    timeout=3):
  """Publishes a text string to a Vestaboard.

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

  Args:




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




      '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)
    UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, True)
    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))
    UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, True)
    error_code = True

  curl.close()
  if not error_code:
    UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, False)


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




  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)
  all_files = os.listdir(directory)
  files = sorted([os.path.join(directory, f) for f in all_files if file in f])
  for f in files:

    print('Bootstrapping %s' % f)
    configuration = ReadAndParseSettings(CONFIG_FILE)
    flights = []
    tmp_f = f + 'tmp'

    if os.path.exists(tmp_f):
      os.remove(tmp_f)

    if os.path.exists(f):
      mtime = os.path.getmtime(f)
      flights = UnpickleObjectFromFile(f, False)
      for (n, flight) in enumerate(flights):
        if n/25 == int(n/25):
          print(' - %d' % n)
        CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
        PickleObjectToFile(flight, tmp_f, False)

      if mtime == os.path.getmtime(f):
        shutil.move(tmp_f, f)
      else:
        print('Aborted: failed to bootstrap %s: file changed while in process' % full_path)
        return


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 os.path.exists(f):
        os.remove(f)
        open(f, 'a').close()
    config.pop('reset_logs')
    config = BuildSettings(config)
    WriteFile(CONFIG_FILE, config)
  return config


def CheckTemperature(fan_power):
  """Turn on fan if temperature exceeds threshold."""
  if RASPBERRY_PI:
    temperature = gpiozero.CPUTemperature().temperature
    if temperature > TEMP_FAN_TURN_ON_CELSIUS and not fan_power:
      fan_power = True
      RPi.GPIO.output(GPIO_FAN, RPi.GPIO.HIGH)
      Log('Fan turned on at temperature %.1f' % temperature)
    elif temperature < TEMP_FAN_TURN_OFF_CELSIUS and fan_power:
      fan_power = False
      RPi.GPIO.output(GPIO_FAN, RPi.GPIO.LOW)
      Log('Fan turned off at temperature %.1f' % temperature)
  return fan_power


pin_values = {}
def SetPinsAsOutput():
  """Initialize output GPIO pins for output on Raspberry Pi."""
  global pin_values




  for pin in (
      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):









    if RASPBERRY_PI:
      RPi.GPIO.setup(pin, RPi.GPIO.OUT)
      RPi.GPIO.output(pin, RPi.GPIO.LOW)
    pin_values[pin] = False








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





  if RASPBERRY_PI:

    if value:
      RPi.GPIO.output(pin, RPi.GPIO.HIGH)

    else:
      RPi.GPIO.output(pin, RPi.GPIO.LOW)




  if pin_values[pin] != value:
    Log('Error light on pin %d set to %s' % (pin, value))
    pin_values[pin] = value










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


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

  if RASPBERRY_PI:
    RPi.GPIO.setmode(RPi.GPIO.BCM)
  SetPinsAsOutput()
  fan_power = False

  configuration = ReadAndParseSettings(CONFIG_FILE)
  startup_time = time.time()
  json_desc_dict = {}

  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)

  # 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




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




           configuration.get('log_jsons', False))

      if flight:
        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_INTERESTING]
          if messages_to_delete:
            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_INTERESTING]

          # 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_INTERESTING, m) for m in insight_messages]




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





        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)
    if os.path.exists(HISTOGRAM_CONFIG_FILE):
      os.remove(HISTOGRAM_CONFIG_FILE)

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

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

    reboot = CheckRebootNeeded(startup_time, message_queue, json_desc_dict)

    fan_power = CheckTemperature(fan_power)

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1
      if simulation_slowdown:
        SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)
    if SHUTDOWN_SIGNAL:  # do a graceful exit
      PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown)

  if SIMULATION:
    SimulationEnd(message_queue, flights)
  PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)


if __name__ == "__main__":
  #interrupt, as in ctrl-c
  signal.signal(signal.SIGINT, RequestGracefulShutdown)

  #terminate, when another instance found or via kill
  signal.signal(signal.SIGTERM, RequestGracefulShutdown)

  if '-i' in sys.argv:
    BootstrapInsightList()
  else:
    main()

01234567890123456789012345678901234567890123456789012345678901234567890123456789









19202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667








153154155156157158159160161162163164165166167168169170171172          173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224








247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359  360361362363364365366367368369370371372373374375376377378379380381382383384385








9779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024








1174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216








130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383








45404541454245434544454545464547454845494550455145524553455445554556455745584559 4560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617  461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767   47684769477047714772477347744775477647774778477947804781478247834784478547864787








4795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822   482348244825482648274828482948304831483248334834483548364837483848394840484148424843








48974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937








49564957495849594960496149624963496449654966496749684969497049714972497349744975 497649774978497949804981 498249834984 498549864987498849894990499149924993499449954996499749984999500050015002500350045005








503150325033503450355036503750385039504050415042504350445045504650475048504950505051 50525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074 507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191  51925193519451955196519751985199520052015202520352045205520652075208520952105211








52745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314








53215322532353245325532653275328532953305331533253335334533553365337533853395340 5341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383



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




import bs4
import dateutil.relativedelta
import filelock
import numpy
import matplotlib
import matplotlib.pyplot
import psutil
import pycurl
import pytz
import requests
import tzlocal
import unidecode

# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'

VERBOSE = False  # additional messages logged

RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'
if RASPBERRY_PI:
  import gpiozero  # pylint: disable=E0401
  import RPi.GPIO  # pylint: disable=E0401

SHUTDOWN_SIGNAL = False
REBOOT_SIGNAL = False

SIMULATION = False
SIMULATION_COUNTER = 0
SIMULATION_PREFIX = 'SIM_'
PICKLE_DUMP_JSON_FILE = 'pickle/dump_json.pk'
PICKLE_FA_JSON_FILE = 'pickle/fa_json.pk'
DUMP_JSONS = None  # loaded only if in simulation mode
FA_JSONS = None  # loaded only if in simulation mode

HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29  #altitude in meters
RADIUS = 6371.0e3  # radius of earth in meters

FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444

MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed




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




FLAG_INSIGHT_GROUNDSPEED = 3
FLAG_INSIGHT_ALTITUDE = 4
FLAG_INSIGHT_VERTRATE = 5
FLAG_INSIGHT_FIRST_DEST = 6
FLAG_INSIGHT_FIRST_ORIGIN = 7
FLAG_INSIGHT_FIRST_AIRLINE = 8
FLAG_INSIGHT_FIRST_AIRCRAFT = 9
FLAG_INSIGHT_LONGEST_DELAY = 10
FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11
FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12
FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13
FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14
FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15
FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16
FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17
FLAG_INSIGHT_HOUR_DELAY_TIME = 18
FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19
FLAG_INSIGHT_DATE_DELAY_TIME = 20
INSIGHT_TYPES = 21











TEMP_FAN_TURN_ON_CELSIUS = 65
TEMP_FAN_TURN_OFF_CELSIUS = 55

# GPIO relay connections - (GPIO pin, true message, false message, relay number)
GPIO_ERROR_VESTABOARD_CONNECTION = (
    22, 'Vestaboard unavailable', 'Vestaboard available', 1)
GPIO_ERROR_FLIGHT_AWARE_CONNECTION = (
    23, 'FlightAware not available', 'FlightAware available', 2)
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = (
    24,
    'Servos not running or lost connection',
    'Handshake with servo Arduino received',
    3)
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = (
    25,
    'Remote not running or lost connection',
    'Handshake with remote Arduino received',
    4)
GPIO_ERROR_BATTERY_CHARGE = (26, 'Remote battery is low', 'Remote battery recharged', 5)
GPIO_FAN = (
    5,
    'RPi above %dC degrees; fan switched on' % TEMP_FAN_TURN_ON_CELSIUS,
    'RPi below %dC degrees; fan switched off' % TEMP_FAN_TURN_OFF_CELSIUS, 7)

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

# 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
  LOGFILE = MESSAGEBOARD_PATH + LOGFILE
  PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
  PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE

  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

  HISTOGRAM_IMAGE_PREFIX = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX
  HISTOGRAM_EMPTY_IMAGE_FILE = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)




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




SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
MINUTES_IN_DAY = MINUTES_IN_HOUR * HOURS_IN_DAY
SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY

# Units confirmed here:
# www.adsbexchange.com/forum/threads/units-in-the-dump1090-json-file.630617/#post-639541
CLIMB_RATE_UNITS = 'fpm'
#speed units from tracker are knots, based on dump-1090/track.c
#https://github.com/SDRplay/dump1090/blob/master/track.c
SPEED_UNITS = 'kn'
DISTANCE_UNITS = 'ft'  # altitude

# For displaying histograms
# If a key is not present, how should it be displayed in histograms?
KEY_NOT_PRESENT_STRING = 'Unknown'
OTHER_STRING = 'Other' # What key strings should be listed last in sequence?
# What key strings should be listed last in sequence?
SORT_AT_END_STRINGS = [OTHER_STRING, KEY_NOT_PRESENT_STRING]
# What is the sorted sequence of keys for days of week?
DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']

import arduino  # module expects paths to be set before import  # pylint: disable=C0413

aircraft_length = {} # in meters
aircraft_length['Airbus A220-100 (twin-jet)'] = 35
aircraft_length['Airbus A300F4-600 (twin-jet)'] = 54.08
aircraft_length['Airbus A319 (twin-jet)'] = 33.84
aircraft_length['Airbus A320 (twin-jet)'] = 37.57
aircraft_length['Airbus A320neo (twin-jet)'] = 37.57
aircraft_length['Airbus A321 (twin-jet)'] = 44.51
aircraft_length['Airbus A321neo (twin-jet)'] = 44.51
aircraft_length['Airbus A330-200 (twin-jet)'] = 58.82
aircraft_length['Airbus A330-300 (twin-jet)'] = 63.67
aircraft_length['Airbus A340-300 (quad-jet)'] = 63.69
aircraft_length['Airbus A350-1000 (twin-jet)'] = 73.79
aircraft_length['Airbus A350-900 (twin-jet)'] = 66.8
aircraft_length['Airbus A380-800 (quad-jet)'] = 72.72
aircraft_length['Boeing 737-400 (twin-jet)'] = 36.4
aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92
aircraft_length['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
aircraft_length['Embraer Phenom 300 (twin-jet)'] = 15.9
aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46
aircraft_length['Cessna Citation CJ2+ (twin-jet)'] = 14.53
aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Citation X (twin-jet)'] = 22.04
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
aircraft_length['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
aircraft_length['Learjet 45 (twin-jet)'] = 17.68
aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4


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

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

  # special case: for the main logfile, we always keep a rolling log
  if not rolling and file == LOGFILE:
    rolling = ROLLING_LOGFILE

  #if file == LOGFILE:
  #  lock = filelock.FileLock(LOGFILE_LOCK)
  #  lock.acquire()

  try:
    with open(file, 'a') as f:
      # by excluding the timestamp, file diffs become easier between runs
      if not SIMULATION or file == LOGFILE:
        f.write('='*80+'\n')
        f.write(str(datetime.datetime.now(TZ))+'\n')
        f.write('\n')
      f.write(str(message)+'\n')
  except IOError:
    Log('Unable to append to ' + file)

  if rolling:


    existing_log_lines = ReadFile(file).splitlines()
    with open(rolling, 'w') as f:
      f.write('\n'.join(existing_log_lines[-1000:]))

  #if file == LOGFILE:
  #  lock.release()


def MaintainRollingWebLog(message, max_count, filename=None):
  """Maintains a rolling text file of at most max_count printed messages.

  Newest data at top and oldest data at the end, of at most max_count messages,
  where the delimiter between each message is identified by a special fixed string.

  Args:
    message: text message to prepend to the file.
    max_count: maximum number of messages to keep in the file; the max_count+1st message
      is deleted.
    filename: the file to update.
  """
  # can't define as a default parameter because ROLLING_MESSAGE_FILE name is potentially
  # modified based on SIMULATION flag
  if not filename:
    filename = ROLLING_MESSAGE_FILE
  rolling_log_header = '='*(SPLITFLAP_CHARS_PER_LINE + 2)
  existing_file = ReadFile(filename)




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




    available or if delimiters missing.
  """
  settings_dict = {}
  for setting in settings.split(';'):
    if '=' in setting:
      kv_list = setting.split('=')
      k = kv_list[0]
      v = kv_list[1]
      if v.isdigit():
        v = int(v)
      else:
        try:
          v = float(v)
        except ValueError:
          pass
      settings_dict[k] = v

  return settings_dict


def RemoveSetting(configuration, setting):
  """Removes the named setting from the configuration file."""
  configuration.pop(setting)
  configuration = BuildSettings(configuration)
  WriteFile(CONFIG_FILE, configuration)
  return configuration


def WriteFile(filename, text, log_exception=False):
  """Writes the text to the file, returning boolean indicating success.

  Args:
    filename: string of the filename to open, potentially also including the full path.
    text: the text to write
    log_exception: boolean indicating whether to log an exception if file not found.

  Returns:
    Boolean indicating whether the write was successful.
  """
  try:
    with open(filename, 'w') as content_file:
      content_file.write(text)
  except IOError:
    if log_exception:
      Log('Unable to write to '+filename)
    return False
  return True





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




    newly_nearby_flight_numbers = UpdateAircraftList(
        persistent_nearby_aircraft, current_nearby_aircraft, now)

    if newly_nearby_flight_numbers:

      if len(newly_nearby_flight_numbers) > 1:
        newly_nearby_flight_numbers_str = ', '.join(newly_nearby_flight_numbers)
        newly_nearby_flight_details_str = '\n'.join(
            [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_numbers])
        Log('Multiple newly-nearby flights: %s\n%s' % (
            newly_nearby_flight_numbers_str, newly_nearby_flight_details_str))
      flight_number = newly_nearby_flight_numbers[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_number:
        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_number])

      # Augment with the past location data
      flight_details['persistent_path'] = persistent_path[flight_number][1]

  return (
      persistent_nearby_aircraft,
      flight_details,
      now,




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





        if aircraft.get('squawk') is not None:
          simplified_aircraft['squawk'] = aircraft.get('squawk')

        track = aircraft.get('track')
        if isinstance(track, numbers.Number):
          min_meters = MinMetersToHome((lat, lon), track)
          simplified_aircraft['track'] = track
          simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER

        if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
          nearby_aircraft[flight_number] = simplified_aircraft

        # keep all that track info - once we start reporting on a nearby flight, it will
        # become part of the flight's persistent record. Also, note that as we are
        # building a list of tracks for each flight, and we are later assigning the
        # flight dictionary to point to the list, we just simply need to continue
        # updating this list to keep the dictionary up to date (i.e.: we don't need
        # to directly touch the flights dictionary in main).
        (last_seen, current_path) = persistent_path.get(flight_number, (None, []))
        if (  # flight position has been updated with this radio signal
            not current_path or
            simplified_aircraft.get('lat') != current_path[-1].get('lat') or
            simplified_aircraft.get('lon') != current_path[-1].get('lon')):
          current_path.append(simplified_aircraft)
        persistent_path[flight_number] = (now, current_path)

  # if the flight was last seen too far in the past, remove the track info
  for flight_number in list(persistent_path.keys()):
    (last_seen, current_path) = persistent_path[flight_number]
    if last_seen < now - PERSISTENCE_SECONDS:
      persistent_path.pop(flight_number)

  return (nearby_aircraft, now, json_desc_dict, persistent_path)


def GetFlightAwareJson(flight_number):
  """Scrapes the text json message from FlightAware for a given flight number.

  Given a flight number, loads the corresponding FlightAware webpage for that flight and
  extracts the relevant script that contains all the flight details from that page.

  Args:
    flight_number: text flight number (i.e.: SWA1234)

  Returns:
    Text representation of the json message from FlightAware.
  """
  url = 'https://flightaware.com/live/flight/' + flight_number
  try:
    response = requests.get(url)
  except requests.exceptions.RequestException as e:
    Log('Unable to query FA for URL due to %s: %s' % (e, url))
    return ''
  soup = bs4.BeautifulSoup(response.text, 'html.parser')
  l = soup.find_all('script')
  flight_script = None
  for script in l:
    if "trackpollBootstrap" in str(script):
      flight_script = str(script)
      break
  if not flight_script:
    Log('Unable to find trackpollBootstrap script in page: ' + response.text)
    return ''
  first_open_curly_brace = flight_script.find('{')
  last_close_curly_brace = flight_script.rfind('}')
  flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1]
  return flight_json


def Unidecode(s):
  """Convert a special unicode characters to closest ASCII representation."""
  if s is not None:
    s = unidecode.unidecode(s)
  return s




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




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

    while message_queue:
      ManageMessageQueue(message_queue, 0, {'setting_delay': 0})
    SaveFlightsByAltitudeDistanceCSV(flights)
    SaveFlightsToCSV(flights)

  # repickle to a new .pk with full track info
  file_parts = PICKLE_FLIGHTS.split('.')
  new_pickle_file = '.'.join([file_parts[0] + '_full_path', file_parts[1]])

  RemoveFile(new_pickle_file)
  for flight in flights:
    PickleObjectToFile(flight, new_pickle_file, False)

  print('Simulation complete after %s dump json messages processed' % len(DUMP_JSONS))


def SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft):
  """Slows down simulations when a reported-upon flight is nearby."""
  if flights and flights[-1].get('flight_number') in persistent_nearby_aircraft:
    time.sleep(arduino.WRITE_DELAY_TIME)


def DumpJsonChanges():
  """Identifies if sequential dump json files changes, for simulation optimization.

  If we are logging the radio output faster than it is updating, then there will be
  sequential log files in the json list that are identical; we only need to process the
  first of these, and can ignore subsequent ones, without any change of output in the
  simulation results. This function identifies whether the current active json changed
  from the prior one.

  Returns:
    Boolean - True if different (and processing needed), False if identical
  """
  if SIMULATION_COUNTER == 0:
    return True
  (this_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER]
  (last_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER - 1]
  return this_json != last_json


def CheckRebootNeeded(startup_time, message_queue, json_desc_dict, configuration):
  """Reboot based on duration instance has been running.

  Reboot needed in one of the following situations:
  - All quiet: if running for over 24 hours and all is quiet (message queue empty and
    no planes in radio).
  - Mostly quiet: if running for over 36 hours and message queue is empty and it's 3a.
  - Reboot requested via html form.

  Also checks if reset requested via html form.
  """
  reboot = False
  end_process = False

  running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
  if (
      running_hours >= HOURS_IN_DAY and
      not message_queue and
      not json_desc_dict.get('radio_range_flights')):
    reboot = True
    Log('All quiet reboot needed after running for %.2f hours' % running_hours)
  if (
      running_hours > HOURS_IN_DAY * 1.5 and
      not message_queue and
      int(EpochDisplayTime(time.time(), '%-H')) >= 3):
    reboot = True


    Log('Early morning reboot needed after running for %.2f hours' % running_hours)
  if 'soft_reboot' in configuration:
    reboot = True
    Log('Soft reboot requested via web form')
    RemoveSetting(configuration, 'soft_reboot')

  if 'end_process' in configuration:
    Log('Process end requested via web form')
    RemoveSetting(configuration, 'end_process')
    end_process = True

  if reboot or end_process:
    global SHUTDOWN_SIGNAL
    SHUTDOWN_SIGNAL = True

  return reboot


def InterruptRebootFromButton():
  """Sets flag so that the main loop will terminate when it completes the iteration.

  This function is only triggered by an physical button press.
  """
  global SHUTDOWN_SIGNAL
  SHUTDOWN_SIGNAL = True

  global REBOOT_SIGNAL
  REBOOT_SIGNAL = True

  RPi.GPIO.output(GPIO_SOFT_RESET[1], False)  # signal that reset received
  Log('Soft reboot requested by button push')


def InterruptShutdownFromSignal(signalNumber, unused_frame):
  """Sets flag so that the main loop will terminate when it completes the iteration.

  The function signature is defined by the python language - i.e.: these two variables
  are passed automatically for registered signals.  This function is only triggered by an
  interrupt signal.
  """
  global SHUTDOWN_SIGNAL
  SHUTDOWN_SIGNAL = True
  Log('%d received termination signal %d (%s)' % (
      os.getpid(), signalNumber,
      signal.Signals(signalNumber).name))  # pylint: disable=E1101


def PerformGracefulShutdown(queues, shutdown, reboot):
  """Complete the graceful shutdown process by cleaning up.

  Args:
    queues: iterable of queues shared with child processes to be closed
    shutdown: tuple of shared flags with child processes to initiate shutdown in children
    reboot: boolean indicating whether we should trigger a reboot
  """
  reboot_msg = ''
  if reboot:
    reboot_msg = ' and rebooting'
  Log('Shutting down self (%d)%s' % (os.getpid(), reboot_msg))

  for q in queues:
    q.close()
  for v in shutdown:  # send the shutdown signal to child processes
    v.value = 1
  if RASPBERRY_PI:
    RPi.GPIO.cleanup()

  if reboot or REBOOT_SIGNAL:
    time.sleep(10)  # wait 10 seconds for children to shut down as well
    os.system('sudo reboot')
  sys.exit()


def FindRunningParents():
  """Returns list of proc ids of processes with identically-named python file running.

  In case there are multiple children processes spawned with the same name, such as via
  multiprocessing, this will only return the parent id (since a killed child process
  will likely just be respawned).
  """
  this_process_id = os.getpid()
  this_process_name = os.path.basename(sys.argv[0])
  pids = []
  pid_pairs = []
  for proc in psutil.process_iter():
    try:
      # Check if process name contains this_process_name.
      commands = proc.as_dict(attrs=['cmdline', 'pid', 'ppid'])
      if commands['cmdline']:
        command_running = any(
            [this_process_name in s for s in commands['cmdline']])
        if command_running:
          pids.append(commands['pid'])
          pid_pairs.append((commands['pid'], commands['ppid']))
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
      pass

  # Exclude those pids that have a parent that is also a pid
  final_pids = []
  for pid_pair in pid_pairs:
    if pid_pair[1] not in pids:
      final_pids.append(pid_pair[0])
  # Exclude this pid
  final_pids.pop(final_pids.index(this_process_id))

  return sorted(final_pids)


def WaitUntilKillComplete(already_running_ids, max_seconds=30):
  """Prevents main loop from starting until other instance, if any, completes shutdown.

  A termination command send to any other identically-named process may take a few
  seconds to complete because that other process is allowed to finish the current iteration
  in the main loop. Typically, that iteration in the other process will complete before
  this process finishes the initialization and starts. But in limited scenarios, that might
  not happen, such as if the other process is in the middle of generating a lot of
  histogram images, or if this process does not have much data to load.

  This function ensures that this process does not start the main loop until the other
  process terminates. If it detects that the other process is still running, it waits
  for up to max_seconds. If the other process does not terminate before that time limit,
  then this restarts the RPi.
  """
  still_running_ids = FindRunningParents()
  if not still_running_ids:
    return
  # still_running_ids should at least be a subset of already_running_ids
  new_processes = sorted(list(set(still_running_ids).difference(set(already_running_ids))))
  if new_processes:
    # uh-oh! at least one new started up in the interim? exit!
    Log('Kill signal sent to %s from this process %s, but it seems like there is '
        'at least one new process running, %s!' % (
            str(already_running_ids), str(os.getpid()), str(new_processes)))
    sys.exit()
  # phew - they're a subset; so they probably got the signal; just wait a few secs
  elif still_running_ids:
    n = 0
    running_parents = FindRunningParents()
    while running_parents:
      if n == max_seconds:
        Log('Kill signal sent from this process %d to %s, but %s still '
            'running after waiting cume %d seconds; rebooting' % (
                os.getpid(), str(already_running_ids), str(running_parents), n+1))
        PerformGracefulShutdown((), (), True)
      if not n % 3:
        Log('Kill signal sent from this process %d to %s, but %s still '
            'running after waiting cume %d seconds' % (
                os.getpid(), str(already_running_ids), str(running_parents), n))
      n += 1
      time.sleep(1)



      running_parents = FindRunningParents()


def InitArduinos(configuration):
  """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)

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


def RefreshArduinos(
    remote, servo,
    to_remote_q, to_servo_q, to_main_q, shutdown,




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





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."""
  remote = ValidateSingleRunning(
      'enable_remote' in configuration,
      arduino.RemoteMain, p=remote,
      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
      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 = dict(flights[-1])

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






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




      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_INTERESTING]
        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':
      WriteFile(CONFIG_FILE, *args)

    else:
      Log('Improper command from arduinos: %s / %s' % (command, args))

  return message_queue, next_message_time


def PublishMessage(
    s,
    subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
    key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
    secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
    timeout=3):
  """Publishes a text string to a Vestaboard.

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

  Args:




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




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




  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)
  all_files = os.listdir(directory)
  files = sorted([os.path.join(directory, f) for f in all_files if file in f])
  for f in files:

    print('Bootstrapping %s' % f)
    configuration = ReadAndParseSettings(CONFIG_FILE)
    flights = []
    tmp_f = f + 'tmp'

    RemoveFile(tmp_f)


    if os.path.exists(f):
      mtime = os.path.getmtime(f)
      flights = UnpickleObjectFromFile(f, False)
      for (n, flight) in enumerate(flights):
        if n/25 == int(n/25):
          print(' - %d' % n)
        CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
        PickleObjectToFile(flight, tmp_f, False)

      if mtime == os.path.getmtime(f):
        shutil.move(tmp_f, f)
      else:
        print('Aborted: failed to bootstrap %s: file changed while in process' % full_path)
        return


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(fan_power):
  """Turn on fan if temperature exceeds threshold."""
  if RASPBERRY_PI:
    temperature = gpiozero.CPUTemperature().temperature
    if temperature > TEMP_FAN_TURN_ON_CELSIUS and not fan_power:
      fan_power = True
      RPi.GPIO.output(GPIO_FAN, True)
      Log('Fan turned on at temperature %.1f' % temperature)
    elif temperature < TEMP_FAN_TURN_OFF_CELSIUS and fan_power:
      fan_power = False
      RPi.GPIO.output(GPIO_FAN, False)
      Log('Fan turned off at temperature %.1f' % temperature)
  return fan_power


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)

  for pin in (
      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):

    # Initialize some pins to start in error condition
    if pin in (
        GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
        GPIO_UNUSED_1, GPIO_UNUSED_2):
      pin_values[pin[0]] = True
    else:
      pin_values[pin[0]] = False

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

  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:
    Log(msg)
    pin_values[pin[0]] = value


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

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


  fan_power = False

  configuration = ReadAndParseSettings(CONFIG_FILE)
  startup_time = time.time()
  json_desc_dict = {}

  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)

  # 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




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




           configuration.get('log_jsons', False))

      if flight:
        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_INTERESTING]
          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_INTERESTING]

          # 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_INTERESTING, m) for m in insight_messages]




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





        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)

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

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

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

    fan_power = CheckTemperature(fan_power)

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1
      if simulation_slowdown:
        SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)
    if SHUTDOWN_SIGNAL:  # do a graceful exit
      PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)

  if SIMULATION:
    SimulationEnd(message_queue, flights)
  PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)


if __name__ == "__main__":
  #interrupt, as in ctrl-c
  signal.signal(signal.SIGINT, InterruptShutdownFromSignal)

  #terminate, when another instance found or via kill
  signal.signal(signal.SIGTERM, InterruptShutdownFromSignal)

  if '-i' in sys.argv:
    BootstrapInsightList()
  else:
    main()