messageboard-2020-05-28-1625.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789
1234567891011 121314151617181920212223242526272829303132333435363738394041424344   4546474849505152535455565758596061626364656667686970717273          7475767778798081828384     858687888990919293949596979899100101102103104








125126127128129130131132133134135136137138139140141142143144 145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 176177178   179180   181182183184185186187188189190191    192193194195 196197198199200201202203204205206207208 209210211 212213214215216217218219220 221222223224225 226227228229230231232233234235236237238239240241242243244245








248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317    318319320321322323324325326327328329330331332333334335336337338339340341342343344








787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834








1013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043  104410451046 104710481049      1050 105110521053 10541055105610571058105910601061    10621063106410651066106710681069107010711072107310741075107610771078107910801081








10831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114  111511161117 111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192 119311941195119611971198119912001201120212031204120512061207120812091210    121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246                                                                     1247124812491250125112521253     125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289                12901291129212931294 12951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320         1321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360








20602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103








32703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310








4460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490        449144924493 449444954496449744984499450045014502450345044505450645074508450945104511451245134514








46644665466646674668466946704671467246734674467546764677467846794680468146824683  46844685468646874688468946904691469246934694469546964697469846994700470147024703








480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856








506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112  51135114511551165117511851195120 5121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151        5152515351545155515651575158515951605161                                                                                                               51625163516451655166516751685169  517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214 5215   5216521752185219  5220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247  5248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275     527652775278527952805281528252835284528552865287528852895290529152925293529452955296








533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383
#!/usr/bin/python3

import datetime
import io
import json
import math
import multiprocessing
import numbers
import os
import pickle
import queue

import shutil
import signal
import statistics
import sys
import textwrap
import time

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
# planes not seen within MIN_METERS in PERSISTENCE_SECONDS seconds will be dropped from
# the nearby list
PERSISTENCE_SECONDS = 10
TRUNCATE = 50  # max number of keys to include in a histogram image file
# number of seconds to pause between each radio poll / command processing loop
LOOP_DELAY_SECONDS = 1











MAX_INSIGHT_HORIZON_DAYS = 30

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

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






CACHED_ELEMENT_PREFIX = 'cached_'

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

# Users can trigger .png histograms analogous to the text ones from the web interface;
# this is the folder (within WEBSERVER_PATH) where those files are placed
WEBSERVER_IMAGE_FOLDER = 'images/'
# Multiple histograms can be generated, i.e. for airline, aircraft, day of week, etc.
# The output files are named by the prefix & suffix, i.e.: prefix + type + . + suffix,
# as in histogram_aircraft.png. These names match up to the names expected by the html
# page that displays the images. Also, note that the suffix is interpreted by matplotlib
# to identify the image format to create.
HISTOGRAM_IMAGE_PREFIX = 'histogram_'




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




# away we anticipate the flight being at its closest point to HOME. As those two
# parameters are manipulated in the settings, a histogram is displayed with one or
# potentially two series, showing the present and potentially prior-set distribution
# of flights, by hour throughout the day, over the last seven days, normalized to
# flights per day. This allows those parameters to be fine-tuned in a useful way.
# This file is the location, on the webserver, of that image, which needs to be in
# alignment with the html page that displays it.
HOURLY_IMAGE_FILE = 'hours.png'

# This is all messages that have been sent to the board since the last time the
# file was manually cleared. Newest messages are at the bottom. It is visible at the
# webserver.
ALL_MESSAGE_FILE = 'all_messages.txt'  #enumeration of all messages sent to board
# This shows the most recent n messages sent to the board. Newest messages are at the
# top for easier viewing of "what did I miss".
ROLLING_MESSAGE_FILE = 'rolling_messages.txt'

STDERR_FILE = 'stderr.txt'
BACKUP_FILE = 'backup.txt'
SERVICE_VERIFICATION_FILE = 'service-verification.txt'


FLAG_MSG_FLIGHT = 1  # basic flight details
FLAG_MSG_INTERESTING = 2  # random tidbit about a flight
FLAG_MSG_HISTOGRAM = 3 # histogram message

FLAG_INSIGHT_LAST_SEEN = 0
FLAG_INSIGHT_DIFF_AIRCRAFT = 1
FLAG_INSIGHT_NTH_FLIGHT = 2
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)
  HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HOURLY_IMAGE_FILE


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

KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK')  # iata codes that we don't need to expand

SPLITFLAP_CHARS_PER_LINE = 22
SPLITFLAP_LINE_COUNT = 6

DIRECTIONS_4 = ['N', 'E', 'S', 'W']
DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
                 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']

HOURS = ['12a', ' 1a', ' 2a', ' 3a', ' 4a', ' 5a', ' 6a', ' 7a',
         ' 8a', ' 9a', '10a', '11a', '12p', ' 1p', ' 2p', ' 3p',
         ' 4p', ' 5p', ' 6p', ' 7p', ' 8p', ' 9p', '10p', '11p']

SECONDS_IN_MINUTE = 60
MINUTES_IN_HOUR = 60




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




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:




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




  return min(potential_distance1, potential_distance2)


def SecondsToHhMm(seconds, colon=False):
  """Converts integer number of seconds to xhym string (i.e.: 7h17m) or to 7:17.

  Args:
    seconds: number of seconds
    colon: controls format; if False, format is 7h17m; if True, format is 7:17.

  Returns:
    String representation of hours and minutes.
  """
  if seconds is None:
    return KEY_NOT_PRESENT_STRING[:3]
  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  if minutes > MINUTES_IN_HOUR:
    hours = int(minutes / MINUTES_IN_HOUR)
    minutes = minutes % MINUTES_IN_HOUR
    if colon:
      text = str(hours) + ':' + str(minutes)
    else:
      text = str(hours) + 'h' + str(minutes) + 'm'
  else:
    if colon:
      text = ':' + str(minutes)
    else:
      text = str(minutes) + 'm'
  return text


def SecondsToHours(seconds):
  """Converts integer number of seconds to xh string (i.e.: 7h).

  Args:
    seconds: number of seconds

  Returns:
    String representation of hours.
  """
  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  hours = round(minutes / MINUTES_IN_HOUR)
  return hours


def SecondsToDdHh(seconds):
  """Converts integer number of seconds to xdyh string (i.e.: 7d17h).





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




  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


def PrependFileName(full_path, prefix):
  """Converts /dir/file.png to /dir/prefixfile.png."""
  directory, file_name = os.path.split(full_path)
  file_name = prefix+file_name
  return os.path.join(directory, file_name)


def UnpickleObjectFromFile(full_path, date_segmentation, max_days=None):
  """Load a repository of pickled flight data into memory.

  Args:
    full_path: name (potentially including path) of the pickled file
    date_segmentation: If true, searches for all files that have a prefix of yyyy-mm-dd
      as a prefix to the file name specified in the full path, and loads them in
      sequence for unpickling; if false, uses the full_path as is and loads just that
      single file.
    max_days: Integer that, if specified, indicates maximum number of days of files to
      load back in; otherwise, loads all.



  Returns:
    Return a list of all the flights, in the same sequence as written to the file.

  """
  if date_segmentation:
    directory, file = os.path.split(full_path)






    files = os.listdir(directory)

    if max_days:  # no need to read any files older than x days
      earliest_date = EpochDisplayTime(time.time() - max_days*SECONDS_IN_DAY, '%Y-%m-%d')
      files = [f for f in files if f[:10] >= earliest_date]

    files = sorted([os.path.join(directory, f) for f in files if file in f])
  else:
    if os.path.exists(full_path):
      files = [full_path]
    else:
      return []

  data = []




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




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




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

  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 numbers, and the values
      are the time the flight was last seen.
    current_nearby_aircraft: dictionary where keys are flight numbers, 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 numbers.
  """
  newly_nearby_flight_numbers = []
  for flight_number in current_nearby_aircraft:


    if flight_number not in persistent_nearby_aircraft:
      newly_nearby_flight_numbers.append(flight_number)
    persistent_nearby_aircraft[flight_number] = now

  flights_to_delete = []
  for flight_number in persistent_nearby_aircraft:
    if (flight_number not in current_nearby_aircraft
        and (now - persistent_nearby_aircraft[flight_number]) > PERSISTENCE_SECONDS):
      flights_to_delete.append(flight_number)
  for flight_number in flights_to_delete:
    del persistent_nearby_aircraft[flight_number]
  return newly_nearby_flight_numbers


def ScanForNewFlights(persistent_nearby_aircraft, persistent_path, log_jsons):
  """Determines if there are any new aircraft in the radio message.

  The radio is continuously dumping new json messages to the Raspberry pi with all the
  flights currently observed. This function picks up the latest radio json, and for
  any new nearby flights - there should generally be at most one new flight on each
  pass through - gets additional flight data from FlightAware and augments the flight
  definition with the relevant fields to keep.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight numbers, and the values
      are the time the flight was last seen.
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.
    log_jsons: boolean indicating whether we should pickle the JSONs.

  Returns:
    A tuple:
    - updated persistent_nearby_aircraft
    - (possibly empty) dictionary of flight attributes of the new flight upon its
      first observation.
    - the time of the radio observation if present; None if no radio dump
    - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest
      observed flight, etc.)
    - persistent_path, a data structure containing past details of a flight's location
      as described in ParseDumpJson
  """
  flight_details = {}
  now = time.time()
  if SIMULATION:
    (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER]
  else:
    dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True)

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

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

    newly_nearby_flight_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,
      json_desc_dict,
      persistent_path)


def DescribeDumpJson(parsed):
  """Generates a dictionary with descriptive attributes about the dump json file.

  Args:
    parsed: The parsed json file.

  Returns:
    Dictionary with attributes about radio range, number of flights seen, etc.
  """
  json_desc_dict = {}
  json_desc_dict['now'] = parsed['now']

  aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS]
  json_desc_dict['radio_range_flights'] = len(aircraft)

  aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a]
  current_distances = [HaversineDistanceMeters(
      HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos]
  current_distances = [
      d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None]
  if current_distances:
    json_desc_dict['radio_range_miles'] = max(current_distances)

  return json_desc_dict







































































def ParseDumpJson(dump_json, persistent_path):
  """Identifies all airplanes within given distance of home from the dump1090 file.

  Since the dump1090 json will have messages from all flights that the antenna has picked
  up, we want to keep only flights that are within a relevant distance to us, and also to
  extract from the full set of data in the json to just the relevant fields for additional
  analysis.






  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()
      if flight_number:
        simplified_aircraft['flight_number'] = flight_number

















    if 'lat' in aircraft and 'lon' in aircraft:
      lat = aircraft['lat']
      lon = aircraft['lon']
      if isinstance(lat, numbers.Number) and isinstance(lon, numbers.Number):

        simplified_aircraft['lat'] = lat
        simplified_aircraft['lon'] = lon

        altitude = aircraft.get('altitude', aircraft.get('alt_baro'))
        if isinstance(altitude, numbers.Number):
          simplified_aircraft['altitude'] = altitude

        speed = aircraft.get('speed', aircraft.get('gs'))
        if speed is not None:
          simplified_aircraft['speed'] = speed

        vert_rate = aircraft.get('vert_rate', aircraft.get('baro_rate'))
        if vert_rate is not None:
          simplified_aircraft['vert_rate'] = vert_rate

        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:




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




    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0] was the
      earliest seen, and flights[-1] is the most recent flight for which we are
      attempting to generate an insight.
    percent_size_difference: the minimum size (i.e.: length) difference for the insight
      to warrant including the size details.

  Returns:
    Printable string message; if no message or insights to generate, then an empty string.
  """
  message = ''
  this_flight = flights[-1]
  this_flight_number = DisplayFlightNumber(this_flight)
  last_seen = [f for f in flights[:-1] if DisplayFlightNumber(f) == this_flight_number]

  # Last time this same flight flew a materially different type of aircraft
  if last_seen and 'flight_number' in this_flight:
    last_flight = last_seen[-1]

    last_aircraft = last_flight.get('aircraft_type_friendly')
    last_aircraft_length = aircraft_length.get(last_aircraft, 0)

    this_aircraft = this_flight.get('aircraft_type_friendly')
    this_aircraft_length = aircraft_length.get(this_aircraft, 0)

    this_likely_commercial_flight = (
        this_flight.get('origin_iata') and this_flight.get('destination_iata'))
    if this_likely_commercial_flight and this_aircraft and not this_aircraft_length:
      Log('%s used in a flight with defined origin & destination but yet is '
          'missing length details' % this_aircraft, file=LOGFILE)

    likely_same_commercial_flight = (
        last_flight.get('origin_iata') == this_flight.get('origin_iata') and
        last_flight.get('destination_iata') == this_flight.get('destination_iata') and
        last_flight.get('airline_call_sign') == this_flight.get('airline_call_sign'))

    this_aircraft_bigger = False
    last_aircraft_bigger = False
    if (likely_same_commercial_flight and
        this_aircraft_length > last_aircraft_length * (1 + percent_size_difference)):
      this_aircraft_bigger = True
      comparative_text = 'larger'
    elif (likely_same_commercial_flight and
          last_aircraft_length > this_aircraft_length * (1 + percent_size_difference)):




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




      min_group_qty=5,
      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the hour
  flight_hours = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY and DisplayTime(
        flight, '%-I%p') == DisplayTime(flights[-1], '%-I%p'):
      flight_hours[DisplayTime(flight, '%-d')] = flight_hours.get(
          DisplayTime(flight, '%-d'), 0) + 1
  min_this_hour_flights = max(3, 0.75 * max(flight_hours.values()))

  # Once we've commented on the insights for an hour or day, we don't want to do it again
  hour_delay_frequency_flag = FLAG_INSIGHT_HOUR_DELAY_FREQUENCY
  hour_delay_time_flag = FLAG_INSIGHT_HOUR_DELAY_TIME
  date_delay_frequency_flag = FLAG_INSIGHT_DATE_DELAY_FREQUENCY
  date_delay_time_flag = FLAG_INSIGHT_DATE_DELAY_TIME
  for flight in flights[:-1]:
    insights = flight['insight_types']
    this_hour = DisplayTime(flights[-1], '%x %-I%p')
    this_day = DisplayTime(flights[-1], '%x')
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_FREQUENCY in insights):
      hour_delay_frequency_flag = None
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_TIME in insights):
      hour_delay_time_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_FREQUENCY in insights):
      date_delay_frequency_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_TIME in insights):
      date_delay_time_flag = None

  def TodaysHour(f):
    f_date = DisplayTime(f, '%x')
    f_hour = DisplayTime(f, '%-I%p')
    if f_date == DisplayTime(flights[-1], '%x'):
      return '%s flights today' % f_hour




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




    for flight in flights:
      flight[function[0]] = function[1](flight)

  # these functions return dictionary of values
  functions = [
      lambda f: FlightAnglesSecondsElapsed(f, 0, '_00s'),
      lambda f: FlightAnglesSecondsElapsed(f, 10, '_10s'),
      lambda f: FlightAnglesSecondsElapsed(f, 20, '_20s'),
      DisplayDepartureTimes]
  for function in functions:
    for flight in flights:
      flight.update(function(flight))

  all_keys = set()
  for f in flights:
    all_keys.update(f.keys())
  all_keys = list(all_keys)
  all_keys.sort()

  keys_logical_order = [
      'now_date', 'now_time', 'now_datetime', 'now', 'flight_number', 'origin_iata',
      'destination_iata', 'altitude', 'min_feet', 'vert_rate', 'speed', 'distance',
      'delay_seconds', 'airline_call_sign', 'aircraft_type_friendly',
      'azimuth_degrees_00s', 'azimuth_degrees_10s', 'azimuth_degrees_20s',
      'altitude_degrees_00s', 'altitude_degrees_10s', 'altitude_degrees_20s',
      'ground_distance_feet_00s', 'ground_distance_feet_10s', 'ground_distance_feet_20s',
      'crow_distance_feet_00s', 'crow_distance_feet_10s', 'crow_distance_feet_20s']
  for key in all_keys:
    if key not in keys_logical_order:
      keys_logical_order.append(key)









  f = open(filename, 'w')
  f.write(','.join(keys_logical_order)+'\n')
  for flight in flights:

    f.write(','.join(['"'+str(flight.get(k))+'"' for k in keys_logical_order])+'\n')
  f.close()


def SimulationSetup():
  """Updates global variable file names and loads in JSON data for simulation runs."""
  # Clear file so that shell tail -f process can continue to point to same file
  def ClearFile(filename):
    if os.path.exists(filename):
      with open(filename, 'w') as f:
        f.write('')

  global SIMULATION
  SIMULATION = True

  global DUMP_JSONS
  DUMP_JSONS = UnpickleObjectFromFile(PICKLE_DUMP_JSON_FILE, True)

  global FA_JSONS
  FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True)





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





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:




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




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


  additional_attributes = {}

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

  additional_attributes['simulation'] = SIMULATION

  message = (last_flight, json_desc_dict, configuration, additional_attributes)
  try:
    if 'enable_servos' in configuration:
      to_servo_q.put(message, block=False)
    if 'enable_remote' in configuration:




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




      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
  # flight (i.e.: flight['insight_types'] for a given flight might look like
  # [1, 2, 7, 9], or [], to indicate which insights were identified; this then
  # transforms that into {0: 25, 1: 18, ...} summing across all flights.

  for flight in flights:



    distribution = flight['insight_types']
    for key in distribution:
      insight_message_distribution[key] = (
          insight_message_distribution.get(key, 0) + 1)



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

  # used in simulation to print the hour of simulation once per simulated hour
  prev_simulated_hour = ''

  persistent_nearby_aircraft = {} # key = flight number; value = last seen epoch
  persistent_path = {}
  histogram = {}

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

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

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

  WaitUntilKillComplete(already_running_ids)

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



    new_configuration = ReadAndParseSettings(CONFIG_FILE)
    CheckForNewFilterCriteria(configuration, new_configuration, message_queue, flights)
    configuration = new_configuration

    ResetLogs(configuration)  # clear the logs if requested

    # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation,
    # then only read & do related processing for the next dump if the last-modified
    # timestamp indicates the file has been updated since it was last read.
    tmp_timestamp = 0
    if not SIMULATION:
      dump_json_exists = os.path.exists(DUMP_JSON_FILE)
      if dump_json_exists:
        tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE)
    if (SIMULATION and DumpJsonChanges()) or (
        not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp):

      last_dump_json_timestamp = tmp_timestamp

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






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




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




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

01234567890123456789012345678901234567890123456789012345678901234567890123456789
123456789101112131415161718192021222324252627282930313233     343536 373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117








138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275








278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321  322323324325326327328329330331332333334335336337338339340341   342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373








816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863








104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125








1127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214  1215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449   1450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507








22072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250








34173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457








4607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670








482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861








496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014








52205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245  52465247   52485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468 546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585








562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672
#!/usr/bin/python3

import datetime
import io
import json
import math
import multiprocessing
import numbers
import os
import pickle
import queue
import re
import shutil
import signal
import statistics
import sys
import textwrap
import time

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

from constants import RASPBERRY_PI, MESSAGEBOARD_PATH, WEBSERVER_PATH






import arduino


if RASPBERRY_PI:
  import gpiozero  # pylint: disable=E0401
  import RPi.GPIO  # pylint: disable=E0401


VERBOSE = False  # additional messages logged

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
# planes not seen within MIN_METERS in PERSISTENCE_SECONDS seconds will be dropped from
# the nearby list
PERSISTENCE_SECONDS = 300
TRUNCATE = 50  # max number of keys to include in a histogram image file
# number of seconds to pause between each radio poll / command processing loop
LOOP_DELAY_SECONDS = 1

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

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

MAX_INSIGHT_HORIZON_DAYS = 30

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

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

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

CACHED_ELEMENT_PREFIX = 'cached_'

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

# Users can trigger .png histograms analogous to the text ones from the web interface;
# this is the folder (within WEBSERVER_PATH) where those files are placed
WEBSERVER_IMAGE_FOLDER = 'images/'
# Multiple histograms can be generated, i.e. for airline, aircraft, day of week, etc.
# The output files are named by the prefix & suffix, i.e.: prefix + type + . + suffix,
# as in histogram_aircraft.png. These names match up to the names expected by the html
# page that displays the images. Also, note that the suffix is interpreted by matplotlib
# to identify the image format to create.
HISTOGRAM_IMAGE_PREFIX = 'histogram_'




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




# away we anticipate the flight being at its closest point to HOME. As those two
# parameters are manipulated in the settings, a histogram is displayed with one or
# potentially two series, showing the present and potentially prior-set distribution
# of flights, by hour throughout the day, over the last seven days, normalized to
# flights per day. This allows those parameters to be fine-tuned in a useful way.
# This file is the location, on the webserver, of that image, which needs to be in
# alignment with the html page that displays it.
HOURLY_IMAGE_FILE = 'hours.png'

# This is all messages that have been sent to the board since the last time the
# file was manually cleared. Newest messages are at the bottom. It is visible at the
# webserver.
ALL_MESSAGE_FILE = 'all_messages.txt'  #enumeration of all messages sent to board
# This shows the most recent n messages sent to the board. Newest messages are at the
# top for easier viewing of "what did I miss".
ROLLING_MESSAGE_FILE = 'rolling_messages.txt'

STDERR_FILE = 'stderr.txt'
BACKUP_FILE = 'backup.txt'
SERVICE_VERIFICATION_FILE = 'service-verification.txt'
UPTIMES_FILE = 'uptimes.html'

FLAG_MSG_FLIGHT = 1  # basic flight details
FLAG_MSG_INTERESTING = 2  # random tidbit about a flight
FLAG_MSG_HISTOGRAM = 3 # histogram message

FLAG_INSIGHT_LAST_SEEN = 0
FLAG_INSIGHT_DIFF_AIRCRAFT = 1
FLAG_INSIGHT_NTH_FLIGHT = 2
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
# format: (GPIO pin, true message, false message, relay number, description)
GPIO_ERROR_VESTABOARD_CONNECTION = (
    22,
    'ERROR: Vestaboard unavailable',
    'SUCCESS: Vestaboard available',
    1, 'Vestaboard connected')
GPIO_ERROR_FLIGHT_AWARE_CONNECTION = (
    23,
    'ERROR: FlightAware not available',
    'SUCCESS: FlightAware available',
    2, 'FlightAware connected')
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = (
    24,
    'ERROR: Servos not running or lost connection',
    'SUCCESS: Handshake with servo Arduino received',
    3, 'Hemisphere connected')
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = (
    25,
    'ERROR: Remote not running or lost connection',
    'SUCCESS: Handshake with remote Arduino received',
    4, 'Remote connected')
GPIO_ERROR_BATTERY_CHARGE = (
    26,
    'ERROR: Remote battery is low',
    'SUCCESS: Remote battery recharged',
    5, 'Remote charged')
GPIO_FAN = (
    5,
    'ERROR: RPi above %dC degrees' % TEMP_FAN_TURN_ON_CELSIUS,
    'SUCCESS: RPi below %dC degrees' % TEMP_FAN_TURN_OFF_CELSIUS,
    7, 'Thermal condition')

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

  HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE
  CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE
  ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE
  ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE
  ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE
  STDERR_FILE = WEBSERVER_PATH + STDERR_FILE
  BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE
  SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE
  UPTIMES_FILE = WEBSERVER_PATH + UPTIMES_FILE

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

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

KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK')  # iata codes that we don't need to expand

SPLITFLAP_CHARS_PER_LINE = 22
SPLITFLAP_LINE_COUNT = 6

DIRECTIONS_4 = ['N', 'E', 'S', 'W']
DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
                 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']

HOURS = ['12a', ' 1a', ' 2a', ' 3a', ' 4a', ' 5a', ' 6a', ' 7a',
         ' 8a', ' 9a', '10a', '11a', '12p', ' 1p', ' 2p', ' 3p',
         ' 4p', ' 5p', ' 6p', ' 7p', ' 8p', ' 9p', '10p', '11p']

SECONDS_IN_MINUTE = 60
MINUTES_IN_HOUR = 60




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




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 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 Latitude (twin-jet)'] = 18.97



AIRCRAFT_LENGTH['Cessna Citation Sovereign (twin-jet)'] = 19.35
AIRCRAFT_LENGTH['Cessna Citation V (twin-jet)'] = 14.91
AIRCRAFT_LENGTH['Cessna Citation X (twin-jet)'] = 22.04
AIRCRAFT_LENGTH['Cessna Skyhawk (piston-single)'] = 8.28
AIRCRAFT_LENGTH['Cessna Skylane (piston-single)'] = 8.84
AIRCRAFT_LENGTH['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
AIRCRAFT_LENGTH['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:




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




  return min(potential_distance1, potential_distance2)


def SecondsToHhMm(seconds, colon=False):
  """Converts integer number of seconds to xhym string (i.e.: 7h17m) or to 7:17.

  Args:
    seconds: number of seconds
    colon: controls format; if False, format is 7h17m; if True, format is 7:17.

  Returns:
    String representation of hours and minutes.
  """
  if seconds is None:
    return KEY_NOT_PRESENT_STRING[:3]
  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  if minutes > MINUTES_IN_HOUR:
    hours = int(minutes / MINUTES_IN_HOUR)
    minutes = minutes % MINUTES_IN_HOUR
    if colon:
      text = '%d:%02d' % (hours, minutes)
    else:
      text = '%dh%d' % (hours, minutes)
  else:
    if colon:
      text = ':%02d' % minutes
    else:
      text = '%dm' % minutes
  return text


def SecondsToHours(seconds):
  """Converts integer number of seconds to xh string (i.e.: 7h).

  Args:
    seconds: number of seconds

  Returns:
    String representation of hours.
  """
  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  hours = round(minutes / MINUTES_IN_HOUR)
  return hours


def SecondsToDdHh(seconds):
  """Converts integer number of seconds to xdyh string (i.e.: 7d17h).





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




  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


def PrependFileName(full_path, prefix):
  """Converts /dir/file.png to /dir/prefixfile.png."""
  directory, file_name = os.path.split(full_path)
  file_name = prefix+file_name
  return os.path.join(directory, file_name)


def UnpickleObjectFromFile(full_path, date_segmentation, max_days=None, filenames=False):
  """Load a repository of pickled data into memory.

  Args:
    full_path: name (potentially including path) of the pickled file
    date_segmentation: If true, searches for all files that have a prefix of yyyy-mm-dd
      as a prefix to the file name specified in the full path, and loads them in
      sequence for unpickling; if false, uses the full_path as is and loads just that
      single file.
    max_days: Integer that, if specified, indicates maximum number of days of files to
      load back in; otherwise, loads all.
    filenames: If true, rather than returning the list of data, returns a list of the
      filenames that would have been read.

  Returns:
    Return a list - either of the data, or of all the file names that would have been
    read.
  """
  if date_segmentation:
    directory, file = os.path.split(full_path)

    d = '[0-9]'
    sep = '-'
    date_format = d*4 + sep + d*2 + sep + d*2  # yyyy-mm-dd
    exp = date_format + sep + file
    pattern = re.compile(exp)
    files = os.listdir(directory)

    if max_days:  # no need to read any files older than x days
      earliest_date = EpochDisplayTime(time.time() - max_days*SECONDS_IN_DAY, '%Y-%m-%d')
      files = [f for f in files if f[:10] >= earliest_date]

    files = sorted([os.path.join(directory, f) for f in files if pattern.match(f)])
  else:
    if os.path.exists(full_path):
      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):
  """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.




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




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

  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).
  """
  newly_nearby_flight_identifiers = []
  for flight_identifier in current_nearby_aircraft:
    flight_number = flight_identifier[0]
    # Only add it to the list once we've received a flight number
    if flight_identifier not in persistent_nearby_aircraft and flight_number:
      newly_nearby_flight_identifiers.append(flight_identifier)
    persistent_nearby_aircraft[flight_identifier] = now

  flights_to_delete = []
  for flight_identifier in persistent_nearby_aircraft:
    if (flight_identifier not in current_nearby_aircraft
        and (now - persistent_nearby_aircraft[flight_identifier]) > PERSISTENCE_SECONDS):
      flights_to_delete.append(flight_identifier)
  for flight_identifier in flights_to_delete:
    del persistent_nearby_aircraft[flight_identifier]
  return newly_nearby_flight_identifiers


def ScanForNewFlights(persistent_nearby_aircraft, persistent_path, log_jsons):
  """Determines if there are any new aircraft in the radio message.

  The radio is continuously dumping new json messages to the Raspberry pi with all the
  flights currently observed. This function picks up the latest radio json, and for
  any new nearby flights - there should generally be at most one new flight on each
  pass through - gets additional flight data from FlightAware and augments the flight
  definition with the relevant fields to keep.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight numbers, and the values
      are the time the flight was last seen.
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.
    log_jsons: boolean indicating whether we should pickle the JSONs.

  Returns:
    A tuple:
    - updated persistent_nearby_aircraft
    - (possibly empty) dictionary of flight attributes of the new flight upon its
      first observation.
    - the time of the radio observation if present; None if no radio dump
    - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest
      observed flight, etc.)
    - persistent_path, a data structure containing past details of a flight's location
      as described in ParseDumpJson
  """
  flight_details = {}
  now = time.time()
  if SIMULATION:
    (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER]
  else:
    dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True)

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



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

    newly_nearby_flight_identifiers = UpdateAircraftList(
        persistent_nearby_aircraft, current_nearby_aircraft, now)

    if newly_nearby_flight_identifiers:

      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,
      flight_details,
      now,
      json_desc_dict,
      persistent_path)


def DescribeDumpJson(parsed):
  """Generates a dictionary with descriptive attributes about the dump json file.

  Args:
    parsed: The parsed json file.

  Returns:
    Dictionary with attributes about radio range, number of flights seen, etc.
  """
  json_desc_dict = {}
  json_desc_dict['now'] = parsed['now']

  aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS]
  json_desc_dict['radio_range_flights'] = len(aircraft)

  aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a]
  current_distances = [HaversineDistanceMeters(
      HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos]
  current_distances = [
      d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None]
  if current_distances:
    json_desc_dict['radio_range_miles'] = max(current_distances)

  return json_desc_dict


def MergedIdentifier(proposed_id, existing_ids):
  flight_number, squawk = proposed_id

  def CheckPartialMatch(value, position):
    if value is not None:
      return [e for e in existing_ids if e[position] == value and e != proposed_id]
    return []

  matches = CheckPartialMatch(flight_number, 0)
  matches.extend(CheckPartialMatch(squawk, 1))

  if not matches:
    return proposed_id, None

  if not flight_number and matches:
    # arbitrarily choose alpha-first non-null flight_number
    matching_flight_numbers = [m[0] for m in matches if m[0] is not None]
    if matching_flight_numbers:
      flight_number = sorted(matching_flight_numbers)[0]
  if not squawk and matches:
    # arbitrarily choose alpha-first non-null squawk
    matching_squawks = [m[1] for m in matches if m[1] is not None]
    if matching_squawks:
      squawk = sorted(matching_squawks)[0]
  id_to_use = (flight_number, squawk)

  verbose = False
  if verbose:
    message_parts = []
    if proposed_id != id_to_use:
      message_parts.append('Proposed id %s replaced with %s' % (proposed_id, id_to_use))
    if id_to_use in matches:
      matches.remove(id_to_use)
    if matches:
      message_parts.append('%s should be merged with %s' % (matches, id_to_use))
    message = '; '.join(message_parts)
    Log(message)

  return id_to_use, matches


def MergePersistentPath(id_to_use, ids_to_merge, persistent_path):
  verbose = False

  path = []
  timestamps = []

  if id_to_use in persistent_path and id_to_use not in ids_to_merge:
    ids_to_merge.append(id_to_use)

  if verbose:
    print('ids_to_merge: %s' % ids_to_merge)

  for i in ids_to_merge:
    if verbose:
      print('path for %s: %s' % (i, persistent_path[i]))
      print('')
    timestamps.append(persistent_path[i][0])
    path.extend(persistent_path[i][1])
    persistent_path.pop(i)
  persistent_path[id_to_use] = (max(timestamps), sorted(path, key=lambda p: p['now']))
  if verbose:
    print('new path path for %s: %s' % (id_to_use, persistent_path[id_to_use]))
    print('='*80)
    print('')

  return persistent_path


def ParseDumpJson(dump_json, persistent_path):
  """Identifies all airplanes within given distance of home from the dump1090 file.

  Since the dump1090 json will have messages from all flights that the antenna has picked
  up, we want to keep only flights that are within a relevant distance to us, and also to
  extract from the full set of data in the json to just the relevant fields for additional
  analysis.

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

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

    identifier = (flight_number, squawk)

    # merge any duplicate flights: since the id for nearby_aircraft & persistent_path is
    # the 2-tuple (flight_number, squawk), it's possible for a flight to add or drop
    # one of those two elements over time as the radio signal comes in / falls out.
    # Let's keep the identifier as the non-null values as soon as one is seen.
    id_to_use, ids_to_merge = MergedIdentifier(identifier, persistent_path.keys())

    # Now we need to rename any flight paths with that partial identifier to have
    # the correct new merged_identifier; bu
    if ids_to_merge:
      persistent_path = MergePersistentPath(id_to_use, ids_to_merge, persistent_path)

    if 'lat' in aircraft and 'lon' in aircraft:
      lat = aircraft['lat']
      lon = aircraft['lon']
      if isinstance(lat, numbers.Number) and isinstance(lon, numbers.Number):

        simplified_aircraft['lat'] = lat
        simplified_aircraft['lon'] = lon

        altitude = aircraft.get('altitude', aircraft.get('alt_baro'))
        if isinstance(altitude, numbers.Number):
          simplified_aircraft['altitude'] = altitude

        speed = aircraft.get('speed', aircraft.get('gs'))
        if speed is not None:
          simplified_aircraft['speed'] = speed

        vert_rate = aircraft.get('vert_rate', aircraft.get('baro_rate'))
        if vert_rate is not None:
          simplified_aircraft['vert_rate'] = vert_rate




        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[id_to_use] = simplified_aircraft
          if flight_number:
            nearby_aircraft[id_to_use]['flight_number'] = flight_number
          if squawk:
            nearby_aircraft[id_to_use]['squawk'] = squawk
          # aircraft classification:
          # https://github.com/wiedehopf/adsb-wiki/wiki/ADS-B-aircraft-categories
          category = aircraft.get('category')
          if category is not None:
            simplified_aircraft['category'] = category

        # 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(id_to_use, (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[id_to_use] = (now, current_path)

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

  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:




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




    flights: the list of the raw data from which the insights will be generated,
      where the flights are listed in order of observation - i.e.: flights[0] was the
      earliest seen, and flights[-1] is the most recent flight for which we are
      attempting to generate an insight.
    percent_size_difference: the minimum size (i.e.: length) difference for the insight
      to warrant including the size details.

  Returns:
    Printable string message; if no message or insights to generate, then an empty string.
  """
  message = ''
  this_flight = flights[-1]
  this_flight_number = DisplayFlightNumber(this_flight)
  last_seen = [f for f in flights[:-1] if DisplayFlightNumber(f) == this_flight_number]

  # Last time this same flight flew a materially different type of aircraft
  if last_seen and 'flight_number' in this_flight:
    last_flight = last_seen[-1]

    last_aircraft = last_flight.get('aircraft_type_friendly')
    last_aircraft_length = AIRCRAFT_LENGTH.get(last_aircraft, 0)

    this_aircraft = this_flight.get('aircraft_type_friendly')
    this_aircraft_length = AIRCRAFT_LENGTH.get(this_aircraft, 0)

    this_likely_commercial_flight = (
        this_flight.get('origin_iata') and this_flight.get('destination_iata'))
    if this_likely_commercial_flight and this_aircraft and not this_aircraft_length:
      Log('%s used in a flight with defined origin & destination but yet is '
          'missing length details' % this_aircraft, file=LOGFILE)

    likely_same_commercial_flight = (
        last_flight.get('origin_iata') == this_flight.get('origin_iata') and
        last_flight.get('destination_iata') == this_flight.get('destination_iata') and
        last_flight.get('airline_call_sign') == this_flight.get('airline_call_sign'))

    this_aircraft_bigger = False
    last_aircraft_bigger = False
    if (likely_same_commercial_flight and
        this_aircraft_length > last_aircraft_length * (1 + percent_size_difference)):
      this_aircraft_bigger = True
      comparative_text = 'larger'
    elif (likely_same_commercial_flight and
          last_aircraft_length > this_aircraft_length * (1 + percent_size_difference)):




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




      min_group_qty=5,
      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the hour
  flight_hours = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY and DisplayTime(
        flight, '%-I%p') == DisplayTime(flights[-1], '%-I%p'):
      flight_hours[DisplayTime(flight, '%-d')] = flight_hours.get(
          DisplayTime(flight, '%-d'), 0) + 1
  min_this_hour_flights = max(3, 0.75 * max(flight_hours.values()))

  # Once we've commented on the insights for an hour or day, we don't want to do it again
  hour_delay_frequency_flag = FLAG_INSIGHT_HOUR_DELAY_FREQUENCY
  hour_delay_time_flag = FLAG_INSIGHT_HOUR_DELAY_TIME
  date_delay_frequency_flag = FLAG_INSIGHT_DATE_DELAY_FREQUENCY
  date_delay_time_flag = FLAG_INSIGHT_DATE_DELAY_TIME
  for flight in flights[:-1]:
    insights = flight.get('insight_types', [])
    this_hour = DisplayTime(flights[-1], '%x %-I%p')
    this_day = DisplayTime(flights[-1], '%x')
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_FREQUENCY in insights):
      hour_delay_frequency_flag = None
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_TIME in insights):
      hour_delay_time_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_FREQUENCY in insights):
      date_delay_frequency_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_TIME in insights):
      date_delay_time_flag = None

  def TodaysHour(f):
    f_date = DisplayTime(f, '%x')
    f_hour = DisplayTime(f, '%-I%p')
    if f_date == DisplayTime(flights[-1], '%x'):
      return '%s flights today' % f_hour




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




    for flight in flights:
      flight[function[0]] = function[1](flight)

  # these functions return dictionary of values
  functions = [
      lambda f: FlightAnglesSecondsElapsed(f, 0, '_00s'),
      lambda f: FlightAnglesSecondsElapsed(f, 10, '_10s'),
      lambda f: FlightAnglesSecondsElapsed(f, 20, '_20s'),
      DisplayDepartureTimes]
  for function in functions:
    for flight in flights:
      flight.update(function(flight))

  all_keys = set()
  for f in flights:
    all_keys.update(f.keys())
  all_keys = list(all_keys)
  all_keys.sort()

  keys_logical_order = [
      'now_date', 'now_time', 'now_datetime', 'now', 'flight_number', 'squawk',
      'origin_iata', 'destination_iata', 'altitude', 'min_feet', 'vert_rate', 'speed',
      'distance', 'delay_seconds', 'airline_call_sign', 'aircraft_type_friendly',
      'azimuth_degrees_00s', 'azimuth_degrees_10s', 'azimuth_degrees_20s',
      'altitude_degrees_00s', 'altitude_degrees_10s', 'altitude_degrees_20s',
      'ground_distance_feet_00s', 'ground_distance_feet_10s', 'ground_distance_feet_20s',
      'crow_distance_feet_00s', 'crow_distance_feet_10s', 'crow_distance_feet_20s']
  for key in all_keys:
    if key not in keys_logical_order:
      keys_logical_order.append(key)

  max_length = 32000
  def ExcelFormatValue(v):
    s = str(v)
    if len(s) > max_length:  # maximum Excel cell length is 32767 characters
      s = '%d character field truncated to %d characters: %s' % (
          len(s), max_length, s[:max_length])
    return s

  f = open(filename, 'w')
  f.write(','.join(keys_logical_order)+'\n')
  for flight in flights:
    f.write(','.join(
        ['"'+ExcelFormatValue(flight.get(k))+'"' for k in keys_logical_order])+'\n')
  f.close()


def SimulationSetup():
  """Updates global variable file names and loads in JSON data for simulation runs."""
  # Clear file so that shell tail -f process can continue to point to same file
  def ClearFile(filename):
    if os.path.exists(filename):
      with open(filename, 'w') as f:
        f.write('')

  global SIMULATION
  SIMULATION = True

  global DUMP_JSONS
  DUMP_JSONS = UnpickleObjectFromFile(PICKLE_DUMP_JSON_FILE, True)

  global FA_JSONS
  FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True)





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





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

  UpdateDashboard(True)

  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:




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




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


  additional_attributes = {}

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

  additional_attributes['simulation'] = SIMULATION

  message = (last_flight, json_desc_dict, configuration, additional_attributes)
  try:
    if 'enable_servos' in configuration:
      to_servo_q.put(message, block=False)
    if 'enable_remote' in configuration:




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




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

  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]])
    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:
    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
  further, if the paths overlap.  If the paths do not overlap, then its likely that
  the same flight was seen some minutes apart, and should legitimately be treated as
  a different flight.

  If the new flight is an updated version, then we should replace the prior-pickled-to-
  disk flight and replace the last flight in flights with this new version.

  Args:
    flight: new flight to check if identical to previous flight
    flights: list of all flights seen so far

  Returns:
    Boolean indicating whether flight is a new (True) or an updated version (False).
  """
  # boundary conditions
  if not flight or not flights:
    return flight

  last_flight = flights[-1]

  # flight_number and squawk are new
  if (
      flight.get('flight_number') != last_flight.get('flight_number')
      and flight.get('squawk') != last_flight.get('squawk')):
    return True

  # its a returning flight... but perhaps some time later as its hovering in the area
  last_flight_last_seen = last_flight.get('persistent_path', [last_flight])[-1]['now']
  if flight['now'] - last_flight_last_seen > PERSISTENCE_SECONDS:
    return True

  # it's not a new flight, so:
  # 1) replace the last flight in flights
  message = (
      'Flight (%s; %s) is overwriting the prior '
      'recorded flight (%s; %s) due to updated identifiers' % (
          flight.get('flight_number'), flight.get('squawk'),
          last_flight.get('flight_number'), last_flight.get('squawk')))
  flights[-1] = flight

  # 2) replace the last pickled record
  #
  # 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)
  saved_flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, not SIMULATION, max_days=max_days)
  saved_flights[-1] = flight
  for f in saved_flights:
    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


def VersionControl():
  global VERSION_MESSAGEBOARD
  global VERSION_ARDUINO

  def MakeCopy(python_prefix):
    file_extension = '.py'

    live_name = python_prefix + '.py'
    live_path = os.path.join(CODE_REPOSITORY, live_name)

    epoch = os.path.getmtime(live_path)
    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()
  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
  # flight (i.e.: flight['insight_types'] for a given flight might look like
  # [1, 2, 7, 9], or [], to indicate which insights were identified; this then
  # transforms that into {0: 25, 1: 18, ...} summing across all flights.
  missing_insights = []
  for flight in flights:
    if 'insight_types' not in flight:
      missing_insights.append(
          '%s on %s' % (DisplayFlightNumber(flight), DisplayTime(flight, '%x %X')))
    distribution = flight.get('insight_types', [])
    for key in distribution:
      insight_message_distribution[key] = (
          insight_message_distribution.get(key, 0) + 1)
  if missing_insights:
    Log('Flights missing insight distributions: %s' % ';'.join(missing_insights))

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

  # used in simulation to print the hour of simulation once per simulated hour
  prev_simulated_hour = ''

  persistent_nearby_aircraft = {} # key = flight number; value = last seen epoch
  persistent_path = {}
  histogram = {}

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

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

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

  WaitUntilKillComplete(already_running_ids)

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

    last_heartbeat_time = Heartbeat(last_heartbeat_time)

    new_configuration = ReadAndParseSettings(CONFIG_FILE)
    CheckForNewFilterCriteria(configuration, new_configuration, message_queue, flights)
    configuration = new_configuration

    ResetLogs(configuration)  # clear the logs if requested

    # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation,
    # then only read & do related processing for the next dump if the last-modified
    # timestamp indicates the file has been updated since it was last read.
    tmp_timestamp = 0
    if not SIMULATION:
      dump_json_exists = os.path.exists(DUMP_JSON_FILE)
      if dump_json_exists:
        tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE)
    if (SIMULATION and DumpJsonChanges()) or (
        not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp):

      last_dump_json_timestamp = tmp_timestamp

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

      # because this might just be an updated instance of the previous flight as more
      # identifier information (squawk and or flight number) comes in, we only want to
      # process this if its a truly new flight
      new_flight_flag = ConfirmNewFlight(flight, flights)

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

        flight_meets_display_criteria = FlightMeetsDisplayCriteria(
            flight, configuration, log=True)
        if flight_meets_display_criteria:
          flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight))

          # display the next message about this flight now!
          next_message_time = time.time()
          message_queue.insert(0, flight_message)
          # and delete any queued insight messages about other flights that have
          # not yet displayed, since a newer flight has taken precedence
          messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INTERESTING]
          if messages_to_delete and VERBOSE:
            Log(
                'Deleting messages from queue due to new-found plane: %s' %




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




      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)

    CheckTemperature()

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1
      if simulation_slowdown:
        SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)
    if 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)  #TODO

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

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