01234567890123456789012345678901234567890123456789012345678901234567890123456789
19202122232425262728293031323334353637383940414243444546 4748495051525354555657585960616263646566 152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 184185186187188189190191192193194195196197198199200201202203 226227228229230231232233234235236237238239240241242243244245 246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 284285286 287288289290291292293294295296297 298299300301302303304305306 307308309310311312 313314315316317318319320321322323324325326327328329330331332333 334335336337338339340341342343344345346347348349350351352353 945946947948949950951952953954955956957958959960961962963964 965966967968969970971972973974975976977978979980981982983984 11341135113611371138113911401141114211431144114511461147114811491150115111521153 1154115511561157115811591160116111621163116411651166116711681169117011711172117311741175 126812691270127112721273127412751276127712781279128012811282128312841285128612871288 128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339 4496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550 4551 455245534554455545564557 45584559456045614562456345644565 456645674568456945704571457245734574 45754576 4577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601 46024603 460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678 4679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703 4711471247134714471547164717471847194720472147224723472447254726472747284729473047314732 4733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757 48114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851 48704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922 494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019 5020502150225023 5024502550265027 50285029503050315032 5033 50345035 50365037 5038503950405041 504250435044504550465047504850495050 5051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093 51565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196 5203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266 |
<----SKIPPED LINES---->
import bs4
import dateutil.relativedelta
import filelock
import numpy
import matplotlib
import matplotlib.pyplot
import psutil
import pycurl
import pytz
import requests
import tzlocal
import unidecode
# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'
import arduino # module expects paths to be set before import # pylint: disable=C0413
RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'
if RASPBERRY_PI:
import gpiozero # pylint: disable=E0401
import RPi.GPIO # pylint: disable=E0401
SHUTDOWN_SIGNAL = False
SIMULATION = False
SIMULATION_COUNTER = 0
SIMULATION_PREFIX = 'SIM_'
PICKLE_DUMP_JSON_FILE = 'pickle/dump_json.pk'
PICKLE_FA_JSON_FILE = 'pickle/fa_json.pk'
DUMP_JSONS = None # loaded only if in simulation mode
FA_JSONS = None # loaded only if in simulation mode
HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29 #altitude in meters
RADIUS = 6371.0e3 # radius of earth in meters
FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444
MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed
<----SKIPPED LINES---->
FLAG_INSIGHT_GROUNDSPEED = 3
FLAG_INSIGHT_ALTITUDE = 4
FLAG_INSIGHT_VERTRATE = 5
FLAG_INSIGHT_FIRST_DEST = 6
FLAG_INSIGHT_FIRST_ORIGIN = 7
FLAG_INSIGHT_FIRST_AIRLINE = 8
FLAG_INSIGHT_FIRST_AIRCRAFT = 9
FLAG_INSIGHT_LONGEST_DELAY = 10
FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11
FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12
FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13
FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14
FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15
FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16
FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17
FLAG_INSIGHT_HOUR_DELAY_TIME = 18
FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19
FLAG_INSIGHT_DATE_DELAY_TIME = 20
INSIGHT_TYPES = 21
# GPIO connections - value is the RPi pin number for the error light
GPIO_ERROR_VESTABOARD_CONNECTION = 22
GPIO_ERROR_FLIGHT_AWARE_CONNECTION = 23
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = 24
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = 25
GPIO_ERROR_BATTERY_CHARGE = 26
GPIO_FAN = 27
GPIO_UNUSED_1 = 5 # for future expansion
GPIO_UNUSED_2 = 6 # for future expansion
TEMP_FAN_TURN_ON_CELSIUS = 65
TEMP_FAN_TURN_OFF_CELSIUS = 55
#if running on raspberry, then need to prepend path to file names
if RASPBERRY_PI:
PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS
LOGFILE = MESSAGEBOARD_PATH + LOGFILE
PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE
HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE
CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE
ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE
ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE
ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE
STDERR_FILE = WEBSERVER_PATH + STDERR_FILE
BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE
SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE
HISTOGRAM_IMAGE_PREFIX = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX
HISTOGRAM_EMPTY_IMAGE_FILE = (
WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)
<----SKIPPED LINES---->
SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
MINUTES_IN_DAY = MINUTES_IN_HOUR * HOURS_IN_DAY
SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY
# Units confirmed here:
# www.adsbexchange.com/forum/threads/units-in-the-dump1090-json-file.630617/#post-639541
CLIMB_RATE_UNITS = 'fpm'
#speed units from tracker are knots, based on dump-1090/track.c
#https://github.com/SDRplay/dump1090/blob/master/track.c
SPEED_UNITS = 'kn'
DISTANCE_UNITS = 'ft' # altitude
# For displaying histograms
# If a key is not present, how should it be displayed in histograms?
KEY_NOT_PRESENT_STRING = 'Unknown'
OTHER_STRING = 'Other' # What key strings should be listed last in sequence?
# What key strings should be listed last in sequence?
SORT_AT_END_STRINGS = [OTHER_STRING, KEY_NOT_PRESENT_STRING]
# What is the sorted sequence of keys for days of week?
DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
aircraft_length = {} # in meters
aircraft_length['Airbus A220-100 (twin-jet)'] = 35
aircraft_length['Airbus A300F4-600 (twin-jet)'] = 54.08
aircraft_length['Airbus A319 (twin-jet)'] = 33.84
aircraft_length['Airbus A320 (twin-jet)'] = 37.57
aircraft_length['Airbus A320neo (twin-jet)'] = 37.57
aircraft_length['Airbus A321 (twin-jet)'] = 44.51
aircraft_length['Airbus A321neo (twin-jet)'] = 44.51
aircraft_length['Airbus A330-200 (twin-jet)'] = 58.82
aircraft_length['Airbus A330-300 (twin-jet)'] = 63.67
aircraft_length['Airbus A340-300 (quad-jet)'] = 63.69
aircraft_length['Airbus A350-1000 (twin-jet)'] = 73.79
aircraft_length['Airbus A350-900 (twin-jet)'] = 66.8
aircraft_length['Airbus A380-800 (quad-jet)'] = 72.72
aircraft_length['Boeing 737-400 (twin-jet)'] = 36.4
aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92
aircraft_length['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46
aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Citation X (twin-jet)'] = 22.04
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
aircraft_length['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4
def Log(message, file=None):
"""Write a message to a logfile along with a timestamp.
Args:
message: string message to write
file: string representing file name and, if needed, path to the file to write to
"""
# can't define as a default parameter because LOGFILE name is potentially
# modified based on SIMULATION flag
if not file:
file = LOGFILE
if file == LOGFILE:
lock = filelock.FileLock(LOGFILE_LOCK)
lock.acquire()
try:
with open(file, 'a') as f:
# by excluding the timestamp, file diffs become easy between runs
if not SIMULATION or file == LOGFILE:
f.write('='*80+'\n')
f.write(str(datetime.datetime.now(TZ))+'\n')
f.write('\n')
f.write(str(message)+'\n')
except IOError:
Log('Unable to append to ' + file)
if file == LOGFILE:
lock.release()
existing_log_lines = ReadFile(LOGFILE).splitlines()
with open(ROLLING_LOGFILE, 'w') as f:
f.write('\n'.join(existing_log_lines[-1000:]))
def MaintainRollingWebLog(message, max_count, filename=None):
"""Maintains a rolling text file of at most max_count printed messages.
Newest data at top and oldest data at the end, of at most max_count messages,
where the delimiter between each message is identified by a special fixed string.
Args:
message: text message to prepend to the file.
max_count: maximum number of messages to keep in the file; the max_count+1st message
is deleted.
filename: the file to update.
"""
# can't define as a default parameter because ROLLING_MESSAGE_FILE name is potentially
# modified based on SIMULATION flag
if not filename:
filename = ROLLING_MESSAGE_FILE
rolling_log_header = '='*(SPLITFLAP_CHARS_PER_LINE + 2)
existing_file = ReadFile(filename)
<----SKIPPED LINES---->
available or if delimiters missing.
"""
settings_dict = {}
for setting in settings.split(';'):
if '=' in setting:
kv_list = setting.split('=')
k = kv_list[0]
v = kv_list[1]
if v.isdigit():
v = int(v)
else:
try:
v = float(v)
except ValueError:
pass
settings_dict[k] = v
return settings_dict
def WriteFile(filename, text, log_exception=False):
"""Writes the text to the file, returning boolean indicating success.
Args:
filename: string of the filename to open, potentially also including the full path.
text: the text to write
log_exception: boolean indicating whether to log an exception if file not found.
Returns:
Boolean indicating whether the write was successful.
"""
try:
with open(filename, 'w') as content_file:
content_file.write(text)
except IOError:
if log_exception:
Log('Unable to write to '+filename)
return False
return True
<----SKIPPED LINES---->
newly_nearby_flight_numbers = UpdateAircraftList(
persistent_nearby_aircraft, current_nearby_aircraft, now)
if newly_nearby_flight_numbers:
if len(newly_nearby_flight_numbers) > 1:
newly_nearby_flight_numbers_str = ', '.join(newly_nearby_flight_numbers)
newly_nearby_flight_details_str = '\n'.join(
[str(current_nearby_aircraft[f]) for f in newly_nearby_flight_numbers])
Log('Multiple newly-nearby flights: %s\n%s' % (
newly_nearby_flight_numbers_str, newly_nearby_flight_details_str))
flight_number = newly_nearby_flight_numbers[0]
flight_aware_json = {}
if SIMULATION:
json_times = [j[1] for j in FA_JSONS]
if json_time in json_times:
flight_aware_json = FA_JSONS[json_times.index(json_time)][0]
elif flight_number:
flight_aware_json = GetFlightAwareJson(flight_number)
UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False)
if not flight_aware_json:
Log('No json returned from Flightaware for flight: %s' % flight_number)
UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True)
flight_details = {}
if flight_aware_json:
flight_details = ParseFlightAwareJson(flight_aware_json)
if not SIMULATION and log_jsons:
PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True)
# Augment FlightAware details with radio / radio-derived details
flight_details.update(current_nearby_aircraft[flight_number])
# Augment with the past location data
flight_details['persistent_path'] = persistent_path[flight_number][1]
return (
persistent_nearby_aircraft,
flight_details,
now,
<----SKIPPED LINES---->
if aircraft.get('squawk') is not None:
simplified_aircraft['squawk'] = aircraft.get('squawk')
track = aircraft.get('track')
if isinstance(track, numbers.Number):
min_meters = MinMetersToHome((lat, lon), track)
simplified_aircraft['track'] = track
simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER
if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
nearby_aircraft[flight_number] = simplified_aircraft
# keep all that track info - once we start reporting on a nearby flight, it will
# become part of the flight's persistent record. Also, note that as we are
# building a list of tracks for each flight, and we are later assigning the
# flight dictionary to point to the list, we just simply need to continue
# updating this list to keep the dictionary up to date (i.e.: we don't need
# to directly touch the flights dictionary in main).
(last_seen, current_path) = persistent_path.get(flight_number, (None, []))
if simplified_aircraft not in current_path:
current_path.append(simplified_aircraft)
persistent_path[flight_number] = (now, current_path)
# if the flight was last seen too far in the past, remove the track info
for flight_number in list(persistent_path.keys()):
(last_seen, current_path) = persistent_path[flight_number]
if last_seen < now - PERSISTENCE_SECONDS:
persistent_path.pop(flight_number)
return (nearby_aircraft, now, json_desc_dict, persistent_path)
def GetFlightAwareJson(flight_number):
"""Scrapes the text json message from FlightAware for a given flight number.
Given a flight number, loads the corresponding FlightAware webpage for that flight and
extracts the relevant script that contains all the flight details from that page.
Args:
flight_number: text flight number (i.e.: SWA1234)
Returns:
Text representation of the json message from FlightAware.
"""
url = 'https://flightaware.com/live/flight/' + flight_number
try:
response = requests.get(url)
except requests.exceptions.RequestException as e:
Log('Unable to query FA for URL due to %s: %s' % (e, url))
return ''
soup = bs4.BeautifulSoup(response.text, "html.parser")
l = soup.find_all('script')
flight_script = None
for script in l:
if "trackpollBootstrap" in str(script):
flight_script = str(script)
break
if not flight_script:
Log('Unable to find trackpollBootstrap script in page: ' + response.text)
return ''
first_open_curly_brace = flight_script.find('{')
last_close_curly_brace = flight_script.rfind('}')
flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1]
return flight_json
def Unidecode(s):
"""Convert a special unicode characters to closest ASCII representation."""
if s is not None:
s = unidecode.unidecode(s)
return s
<----SKIPPED LINES---->
message_queue: List of flight messages that have not yet been printed.
flights: List of flights dictionaries.
"""
if flights:
histogram = {
'type': 'both',
'histogram':'all',
'histogram_history':'30d',
'histogram_max_screens': '_2',
'histogram_data_summary': 'on'}
message_queue.extend(TriggerHistograms(flights, histogram))
while message_queue:
ManageMessageQueue(message_queue, 0, {'setting_delay': 0})
SaveFlightsByAltitudeDistanceCSV(flights)
SaveFlightsToCSV(flights)
# repickle to a new .pk with full track info
file_parts = PICKLE_FLIGHTS.split('.')
new_pickle_file = '.'.join([file_parts[0] + '_full_path', file_parts[1]])
if os.path.exists(new_pickle_file):
os.remove(new_pickle_file)
for flight in flights:
PickleObjectToFile(flight, new_pickle_file, False)
print('Simulation complete after %s dump json messages processed' % len(DUMP_JSONS))
def SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft):
"""Slows down simulations when a reported-upon flight is nearby."""
if flights and flights[-1].get('flight_number') in persistent_nearby_aircraft:
time.sleep(arduino.WRITE_DELAY_TIME)
def DumpJsonChanges():
"""Identifies if sequential dump json files changes, for simulation optimization.
If we are logging the radio output faster than it is updating, then there will be
sequential log files in the json list that are identical; we only need to process the
first of these, and can ignore subsequent ones, without any change of output in the
simulation results. This function identifies whether the current active json changed
from the prior one.
Returns:
Boolean - True if different (and processing needed), False if identical
"""
if SIMULATION_COUNTER == 0:
return True
(this_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER]
(last_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER - 1]
return this_json != last_json
def CheckRebootNeeded(startup_time, message_queue, json_desc_dict):
"""Reboot if running for over 24 hours and all is quiet, or if its just been too long."""
reboot = False
running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
if (
running_hours >= HOURS_IN_DAY and
not message_queue and
not json_desc_dict.get('radio_range_flights')):
reboot = True
if (
running_hours > HOURS_IN_DAY * 1.5 and
not message_queue and
int(EpochDisplayTime(time.time(), '%-H')) >= 3):
reboot = True
if reboot:
Log('Reboot needed after running for %.2f hours' % running_hours)
global SHUTDOWN_SIGNAL
SHUTDOWN_SIGNAL = True
return reboot
def RequestGracefulShutdown(signalNumber, unused_frame):
"""Sets flag so that the main loop will terminate when it completes the iteration.
The function signature is defined by the python language - i.e.: these two variables
are passed automatically for registered signals.
"""
global SHUTDOWN_SIGNAL
SHUTDOWN_SIGNAL = True
Log('%d received termination signal %d (%s)' % (
os.getpid(), signalNumber,
signal.Signals(signalNumber).name)) # pylint: disable=E1101
def PerformGracefulShutdown(queues, shutdown, reboot=False):
"""Complete the graceful shutdown process by cleaning up.
Args:
queues: iterable of queues shared with child processes to be closed
shutdown: tuple of shared flags with child processes to initiate shutdown in children
reboot: boolean indicating whether we should trigger a reboot
"""
reboot_msg = ''
if reboot:
reboot_msg = ' and rebooting'
Log('Shutting down self (%d)%s' % (os.getpid(), reboot_msg))
for q in queues:
q.close()
for v in shutdown: # send the shutdown signal to child processes
v.value = 1
RPi.GPIO.cleanup()
if reboot:
os.system('sudo reboot')
sys.exit()
def FindRunningParents():
"""Returns list of proc ids of processes with identically-named python file running.
In case there are multiple children processes spawned with the same name, such as via
multiprocessing, this will only return the parent id (since a killed child process
will likely just be respawned).
"""
this_process_id = os.getpid()
this_process_name = os.path.basename(sys.argv[0])
pids = []
pid_pairs = []
for proc in psutil.process_iter():
try:
# Check if process name contains this_process_name.
commands = proc.as_dict(attrs=['cmdline', 'pid', 'ppid'])
if commands['cmdline']:
command_running = any(
[this_process_name in s for s in commands['cmdline']])
if command_running:
pids.append(commands['pid'])
pid_pairs.append((commands['pid'], commands['ppid']))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
# Exclude those pids that have a parent that is also a pid
final_pids = []
for pid_pair in pid_pairs:
if pid_pair[1] not in pids:
final_pids.append(pid_pair[0])
# Exclude this pid
final_pids.pop(final_pids.index(this_process_id))
return sorted(final_pids)
def WaitUntilKillComplete(already_running_ids, max_seconds=10):
"""Prevents main loop from starting until other instance, if any, completes shutdown.
A termination command send to any other identically-named process may take a few
seconds to complete because that other process is allowed to finish the current iteration
in the main loop. Typically, that iteration in the other process will complete before
this process finishes the initialization and starts. But in limited scenarios, that might
not happen, such as if the other process is in the middle of generating a lot of
histogram images, or if this process does not have much data to load.
This function ensures that this process does not start the main loop until the other
process terminates. If it detects that the other process is still running, it waits
for up to max_seconds. If the other process does not terminate before that time limit,
then this process terminates.
"""
still_running_ids = FindRunningParents()
if not still_running_ids:
return
# still_running_ids should at least be a subset of already_running_ids
new_processes = sorted(list(set(still_running_ids).difference(set(already_running_ids))))
if new_processes:
# uh-oh! at least one new started up in the interim? exit!
Log('Kill signal sent to %s from this process %s, but it seems like there is '
'at least one new process running, %s!' % (
str(already_running_ids), str(os.getpid()), str(new_processes)))
sys.exit()
# phew - they're a subset; so they probably got the signal; just wait a few secs
elif still_running_ids:
n = 0
running_parents = FindRunningParents()
while running_parents:
if n == max_seconds:
Log('Kill signal sent from this process %d to %s, but %s still '
'running after waiting cume %d seconds; exiting anyways' % (
os.getpid(), str(already_running_ids), str(running_parents), n+1))
sys.exit()
n += 1
time.sleep(1)
Log('Kill signal sent from this process %d to %s, but %s still '
'running after waiting cume %d seconds' % (
os.getpid(), str(already_running_ids), str(running_parents), n+1))
running_parents = FindRunningParents()
def InitArduinos(configuration):
"""Initializes and starts the two arduino threads with new shared-memory queues."""
to_remote_q = multiprocessing.Queue()
to_servo_q = multiprocessing.Queue()
to_main_q = multiprocessing.Queue()
shutdown_remote = multiprocessing.Value('i') # shared flag to initiate shutdown
shutdown_servo = multiprocessing.Value('i') # shared flag to initiate shutdown
shutdown = (shutdown_remote, shutdown_servo)
remote, servo = ValidateArduinosRunning(
None, None, to_remote_q, to_servo_q, to_main_q, shutdown, configuration)
return (remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown)
def RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
<----SKIPPED LINES---->
def ValidateArduinosRunning(
remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration):
"""Ensures that each of the enabled arduinos are running, restarting if needed."""
remote = ValidateSingleRunning(
'enable_remote' in configuration,
arduino.RemoteMain, p=remote,
args=(to_remote_q, to_main_q, shutdown[0]))
servo = ValidateSingleRunning(
'enable_servos' in configuration,
arduino.ServoMain, p=servo,
args=(to_servo_q, to_main_q, shutdown[1]))
return remote, servo
def ValidateSingleRunning(enabled, start_function, p=None, args=()):
"""Restarts a new instance of multiprocessing process if not running"""
if not SHUTDOWN_SIGNAL:
if not enabled:
if p is not None: # must have just shut this down
args[2].value = 1 # signal a shutdown on the shared state arg
else:
return None
elif p is None or not p.is_alive():
Log('Process (%s) for %s is not alive; restarting' % (str(p), str(start_function)))
args[2].value = 0 # (re)set shutdown flag to allow function to run
p = multiprocessing.Process(target=start_function, args=args)
p.daemon = True
p.start()
return p
def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q):
"""Send latest data to arduinos via their shared-memory queues"""
last_flight = {}
if flights:
last_flight = dict(flights[-1])
if SIMULATION:
now = json_desc_dict['now']
else:
now = time.time()
<----SKIPPED LINES---->
messageboard_flight_index = IdentifyFlightDisplayed(
flights, configuration, display_all_hours=True)
if messageboard_flight_index is not None:
message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
message_queue = [(FLAG_MSG_FLIGHT, flight_message)]
next_message_time = time.time()
elif command == 'histogram':
if not flights:
Log('Histogram requested by remote %s but no flights in memory' % str(args))
else:
histogram_type, histogram_history = args
message_queue.extend(MessageboardHistograms(
flights,
histogram_type,
histogram_history,
'_1',
False))
elif command == 'update_settings':
WriteFile(CONFIG_FILE, *args)
else:
Log('Improper command from arduinos: %s / %s' % (command, args))
return message_queue, next_message_time
def PublishMessage(
s,
subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
timeout=3):
"""Publishes a text string to a Vestaboard.
The message is pushed to the vestaboard splitflap display by way of its web services;
see https://docs.vestaboard.com/introduction for more details.
Args:
<----SKIPPED LINES---->
'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id)
curl.setopt(pycurl.HTTPHEADER, [
'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret])
curl.setopt(pycurl.TIMEOUT_MS, timeout*1000)
curl.setopt(pycurl.POST, 1)
curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean
# preparing body the way pycurl.READDATA wants it
body_as_dict = {'text': s}
body_as_json_string = json.dumps(body_as_dict) # dict to json
body_as_file_object = io.StringIO(body_as_json_string)
# prepare and send. See also: pycurl.READFUNCTION to pass function instead
curl.setopt(pycurl.READDATA, body_as_file_object)
curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string))
try:
curl.perform()
except pycurl.error as e:
Log('curl.perform() failed with message %s' % e)
UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, True)
error_code = True
else:
# you may want to check HTTP response code, e.g.
status_code = curl.getinfo(pycurl.RESPONSE_CODE)
if status_code != 200:
Log('Server returned HTTP status code %d for message %s' % (status_code, s))
UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, True)
error_code = True
curl.close()
if not error_code:
UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, False)
def ManageMessageQueue(message_queue, next_message_time, configuration):
"""Check time & if appropriate, display next message from queue.
Args:
message_queue: FIFO list of message tuples of (message type, message string).
next_message_time: epoch at which next message should be displayed
configuration: dictionary of configuration attributes.
Returns:
Next_message_time, potentially updated if a message has been displayed, or unchanged
if no message was displayed.
"""
if message_queue and (time.time() >= next_message_time or SIMULATION):
if SIMULATION: # drain the queue because the messages come so fast
messages_to_display = list(message_queue)
# passed by reference, so clear it out since we drained it to the display
del message_queue[:]
<----SKIPPED LINES---->
be considered correct.
They are "correct" in the sense that that new insight was not available at the time
that older flight was seen, but it is not correct in the sense that, because this new
insight is starting out with an incidence in the historical data of zero, this
new insight may be reported more frequently than desired until it "catches up".
So this method replays the flight history with the latest insight code, regenerating
the insight distribution for each flight.
"""
directory, file = os.path.split(full_path)
all_files = os.listdir(directory)
files = sorted([os.path.join(directory, f) for f in all_files if file in f])
for f in files:
print('Bootstrapping %s' % f)
configuration = ReadAndParseSettings(CONFIG_FILE)
flights = []
tmp_f = f + 'tmp'
if os.path.exists(tmp_f):
os.remove(tmp_f)
if os.path.exists(f):
mtime = os.path.getmtime(f)
flights = UnpickleObjectFromFile(f, False)
for (n, flight) in enumerate(flights):
if n/25 == int(n/25):
print(' - %d' % n)
CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
PickleObjectToFile(flight, tmp_f, False)
if mtime == os.path.getmtime(f):
shutil.move(tmp_f, f)
else:
print('Aborted: failed to bootstrap %s: file changed while in process' % full_path)
return
def ResetLogs(config):
"""Clears the non-scrolling logs if reset_logs in config."""
if 'reset_logs' in config:
Log('Reset logs')
for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE):
if os.path.exists(f):
os.remove(f)
open(f, 'a').close()
config.pop('reset_logs')
config = BuildSettings(config)
WriteFile(CONFIG_FILE, config)
return config
def CheckTemperature(fan_power):
"""Turn on fan if temperature exceeds threshold."""
if RASPBERRY_PI:
temperature = gpiozero.CPUTemperature().temperature
if temperature > TEMP_FAN_TURN_ON_CELSIUS and not fan_power:
fan_power = True
RPi.GPIO.output(GPIO_FAN, RPi.GPIO.HIGH)
Log('Fan turned on at temperature %.1f' % temperature)
elif temperature < TEMP_FAN_TURN_OFF_CELSIUS and fan_power:
fan_power = False
RPi.GPIO.output(GPIO_FAN, RPi.GPIO.LOW)
Log('Fan turned off at temperature %.1f' % temperature)
return fan_power
pin_values = {}
def SetPinsAsOutput():
"""Initialize output GPIO pins for output on Raspberry Pi."""
global pin_values
for pin in (
GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION,
GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN):
if RASPBERRY_PI:
RPi.GPIO.setup(pin, RPi.GPIO.OUT)
RPi.GPIO.output(pin, RPi.GPIO.LOW)
pin_values[pin] = False
def UpdateStatusLight(pin, value):
"""Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value."""
global pin_values
if RASPBERRY_PI:
if value:
RPi.GPIO.output(pin, RPi.GPIO.HIGH)
else:
RPi.GPIO.output(pin, RPi.GPIO.LOW)
if pin_values[pin] != value:
Log('Error light on pin %d set to %s' % (pin, value))
pin_values[pin] = value
def main():
"""Traffic cop between incoming radio flight messages, configuration, and messageboard.
This is the main logic, checking for new flights, augmenting the radio signal with
additional web-scraped data, and generating messages in a form presentable to the
messageboard.
"""
# Since this clears log files, it should occur first before we start logging
if '-s' in sys.argv:
global SIMULATION_COUNTER
SimulationSetup()
# This flag slows down simulation time around a flight, great for debugging the arduinos
simulation_slowdown = bool('-f' in sys.argv)
# Redirect any errors to a log file instead of the screen, and add a datestamp
if not SIMULATION:
sys.stderr = open(STDERR_FILE, 'a')
Log('', STDERR_FILE)
Log('Starting up process %d' % os.getpid())
already_running_ids = FindRunningParents()
if already_running_ids:
for pid in already_running_ids:
Log('Sending termination signal to %d' % pid)
os.kill(pid, signal.SIGTERM)
if RASPBERRY_PI:
RPi.GPIO.setmode(RPi.GPIO.BCM)
SetPinsAsOutput()
fan_power = False
configuration = ReadAndParseSettings(CONFIG_FILE)
startup_time = time.time()
json_desc_dict = {}
flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS)
# Clear the loaded flight of any cached data, identified by keys with a specific
# suffix, since code fixes may change the values for some of those cached elements
for flight in flights:
for key in list(flight.keys()):
if key.endswith(CACHED_ELEMENT_PREFIX):
flight.pop(key)
# If we're displaying just a single insight message, we want it to be something
# unique, to the extent possible; this dict holds a count of the diff types of messages
# displayed so far
insight_message_distribution = {}
# bootstrap the flight insights distribution from a list of insights on each
<----SKIPPED LINES---->
configuration.get('log_jsons', False))
if flight:
flights.append(flight)
remote, servo = RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
flights, json_desc_dict, configuration)
flight_meets_display_criteria = FlightMeetsDisplayCriteria(
flight, configuration, log=True)
if flight_meets_display_criteria:
flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight))
# display the next message about this flight now!
next_message_time = time.time()
message_queue.insert(0, flight_message)
# and delete any queued insight messages about other flights that have
# not yet displayed, since a newer flight has taken precedence
messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INTERESTING]
if messages_to_delete:
Log(
'Deleting messages from queue due to new-found plane: %s' %
messages_to_delete)
message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
# Though we also manage the message queue outside this conditional as well,
# because it can take a half second to generate the flight insights, this allows
# this message to start displaying on the board immediately, so it's up there
# when it's most relevant
next_message_time = ManageMessageQueue(
message_queue, next_message_time, configuration)
insight_messages = CreateFlightInsights(
flights, configuration.get('insights'), insight_message_distribution)
if configuration.get('next_flight', 'off') == 'on':
next_flight_text = FlightInsightNextFlight(flights, configuration)
if next_flight_text:
insight_messages.insert(0, next_flight_text)
insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages]
<----SKIPPED LINES---->
PickleObjectToFile(flight, PICKLE_FLIGHTS, not SIMULATION)
else:
remote, servo = RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
flights, json_desc_dict, configuration)
message_queue, next_message_time = ProcessArduinoCommmands(
to_main_q, flights, configuration, message_queue, next_message_time)
if SIMULATION:
if now:
simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z')
if simulated_hour != prev_simulated_hour:
print(simulated_hour)
prev_simulated_hour = simulated_hour
histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
if os.path.exists(HISTOGRAM_CONFIG_FILE):
os.remove(HISTOGRAM_CONFIG_FILE)
# We also need to make sure there are flights on which to generate a histogram! Why
# might there not be any flights? Primarily during a simulation, if there's a
# lingering histogram file at the time of history restart.
if histogram and not flights:
Log('Histogram requested (%s) but no flights in memory' % histogram)
if histogram and flights:
message_queue.extend(TriggerHistograms(flights, histogram))
# check time & if appropriate, display next message from queue
next_message_time = ManageMessageQueue(message_queue, next_message_time, configuration)
reboot = CheckRebootNeeded(startup_time, message_queue, json_desc_dict)
fan_power = CheckTemperature(fan_power)
if not SIMULATION:
time.sleep(max(0, next_loop_time - time.time()))
next_loop_time = time.time() + LOOP_DELAY_SECONDS
else:
SIMULATION_COUNTER += 1
if simulation_slowdown:
SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)
if SHUTDOWN_SIGNAL: # do a graceful exit
PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown)
if SIMULATION:
SimulationEnd(message_queue, flights)
PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)
if __name__ == "__main__":
#interrupt, as in ctrl-c
signal.signal(signal.SIGINT, RequestGracefulShutdown)
#terminate, when another instance found or via kill
signal.signal(signal.SIGTERM, RequestGracefulShutdown)
if '-i' in sys.argv:
BootstrapInsightList()
else:
main()
|
01234567890123456789012345678901234567890123456789012345678901234567890123456789
19202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 153154155156157158159160161162163164165166167168169170171172 173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 360361362363364365366367368369370371372373374375376377378379380381382383384385 9779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024 1174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216 130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383 45404541454245434544454545464547454845494550455145524553455445554556455745584559 4560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617 461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767 47684769477047714772477347744775477647774778477947804781478247834784478547864787 4795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822 482348244825482648274828482948304831483248334834483548364837483848394840484148424843 48974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937 49564957495849594960496149624963496449654966496749684969497049714972497349744975 497649774978497949804981 498249834984 498549864987498849894990499149924993499449954996499749984999500050015002500350045005 503150325033503450355036503750385039504050415042504350445045504650475048504950505051 50525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074 507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191 51925193519451955196519751985199520052015202520352045205520652075208520952105211 52745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314 53215322532353245325532653275328532953305331533253335334533553365337533853395340 5341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383 |
<----SKIPPED LINES---->
import bs4
import dateutil.relativedelta
import filelock
import numpy
import matplotlib
import matplotlib.pyplot
import psutil
import pycurl
import pytz
import requests
import tzlocal
import unidecode
# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'
VERBOSE = False # additional messages logged
RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'
if RASPBERRY_PI:
import gpiozero # pylint: disable=E0401
import RPi.GPIO # pylint: disable=E0401
SHUTDOWN_SIGNAL = False
REBOOT_SIGNAL = False
SIMULATION = False
SIMULATION_COUNTER = 0
SIMULATION_PREFIX = 'SIM_'
PICKLE_DUMP_JSON_FILE = 'pickle/dump_json.pk'
PICKLE_FA_JSON_FILE = 'pickle/fa_json.pk'
DUMP_JSONS = None # loaded only if in simulation mode
FA_JSONS = None # loaded only if in simulation mode
HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29 #altitude in meters
RADIUS = 6371.0e3 # radius of earth in meters
FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444
MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed
<----SKIPPED LINES---->
FLAG_INSIGHT_GROUNDSPEED = 3
FLAG_INSIGHT_ALTITUDE = 4
FLAG_INSIGHT_VERTRATE = 5
FLAG_INSIGHT_FIRST_DEST = 6
FLAG_INSIGHT_FIRST_ORIGIN = 7
FLAG_INSIGHT_FIRST_AIRLINE = 8
FLAG_INSIGHT_FIRST_AIRCRAFT = 9
FLAG_INSIGHT_LONGEST_DELAY = 10
FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11
FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12
FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13
FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14
FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15
FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16
FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17
FLAG_INSIGHT_HOUR_DELAY_TIME = 18
FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19
FLAG_INSIGHT_DATE_DELAY_TIME = 20
INSIGHT_TYPES = 21
TEMP_FAN_TURN_ON_CELSIUS = 65
TEMP_FAN_TURN_OFF_CELSIUS = 55
# GPIO relay connections - (GPIO pin, true message, false message, relay number)
GPIO_ERROR_VESTABOARD_CONNECTION = (
22, 'Vestaboard unavailable', 'Vestaboard available', 1)
GPIO_ERROR_FLIGHT_AWARE_CONNECTION = (
23, 'FlightAware not available', 'FlightAware available', 2)
GPIO_ERROR_ARDUINO_SERVO_CONNECTION = (
24,
'Servos not running or lost connection',
'Handshake with servo Arduino received',
3)
GPIO_ERROR_ARDUINO_REMOTE_CONNECTION = (
25,
'Remote not running or lost connection',
'Handshake with remote Arduino received',
4)
GPIO_ERROR_BATTERY_CHARGE = (26, 'Remote battery is low', 'Remote battery recharged', 5)
GPIO_FAN = (
5,
'RPi above %dC degrees; fan switched on' % TEMP_FAN_TURN_ON_CELSIUS,
'RPi below %dC degrees; fan switched off' % TEMP_FAN_TURN_OFF_CELSIUS, 7)
# for future expansion
GPIO_UNUSED_1 = (
27, 'Undefined condition set to true', 'Undefined condition set to false', 6)
GPIO_UNUSED_2 = (
6, 'Undefined condition set to true', 'Undefined condition set to false', 8)
# GPIO pushbutton connections - (GPIO pin switch in; GPIO pin LED out)
GPIO_SOFT_RESET = (20, 21)
#if running on raspberry, then need to prepend path to file names
if RASPBERRY_PI:
PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS
LOGFILE = MESSAGEBOARD_PATH + LOGFILE
PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE
HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE
CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE
ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE
ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE
ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE
STDERR_FILE = WEBSERVER_PATH + STDERR_FILE
BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE
SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE
HISTOGRAM_IMAGE_PREFIX = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX
HISTOGRAM_EMPTY_IMAGE_FILE = (
WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)
<----SKIPPED LINES---->
SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
MINUTES_IN_DAY = MINUTES_IN_HOUR * HOURS_IN_DAY
SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY
# Units confirmed here:
# www.adsbexchange.com/forum/threads/units-in-the-dump1090-json-file.630617/#post-639541
CLIMB_RATE_UNITS = 'fpm'
#speed units from tracker are knots, based on dump-1090/track.c
#https://github.com/SDRplay/dump1090/blob/master/track.c
SPEED_UNITS = 'kn'
DISTANCE_UNITS = 'ft' # altitude
# For displaying histograms
# If a key is not present, how should it be displayed in histograms?
KEY_NOT_PRESENT_STRING = 'Unknown'
OTHER_STRING = 'Other' # What key strings should be listed last in sequence?
# What key strings should be listed last in sequence?
SORT_AT_END_STRINGS = [OTHER_STRING, KEY_NOT_PRESENT_STRING]
# What is the sorted sequence of keys for days of week?
DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
import arduino # module expects paths to be set before import # pylint: disable=C0413
aircraft_length = {} # in meters
aircraft_length['Airbus A220-100 (twin-jet)'] = 35
aircraft_length['Airbus A300F4-600 (twin-jet)'] = 54.08
aircraft_length['Airbus A319 (twin-jet)'] = 33.84
aircraft_length['Airbus A320 (twin-jet)'] = 37.57
aircraft_length['Airbus A320neo (twin-jet)'] = 37.57
aircraft_length['Airbus A321 (twin-jet)'] = 44.51
aircraft_length['Airbus A321neo (twin-jet)'] = 44.51
aircraft_length['Airbus A330-200 (twin-jet)'] = 58.82
aircraft_length['Airbus A330-300 (twin-jet)'] = 63.67
aircraft_length['Airbus A340-300 (quad-jet)'] = 63.69
aircraft_length['Airbus A350-1000 (twin-jet)'] = 73.79
aircraft_length['Airbus A350-900 (twin-jet)'] = 66.8
aircraft_length['Airbus A380-800 (quad-jet)'] = 72.72
aircraft_length['Boeing 737-400 (twin-jet)'] = 36.4
aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777 (twin-jet)'] = (63.73 + 73.86) / 2
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92
aircraft_length['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
aircraft_length['Embraer Phenom 300 (twin-jet)'] = 15.9
aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46
aircraft_length['Cessna Citation CJ2+ (twin-jet)'] = 14.53
aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Citation X (twin-jet)'] = 22.04
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
aircraft_length['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
aircraft_length['Learjet 45 (twin-jet)'] = 17.68
aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4
def Log(message, file=None, rolling=None):
"""Write a message to a logfile along with a timestamp.
Args:
message: string message to write
file: string representing file name and, if needed, path to the file to write to
rolling: name of file that will keep only the last n files of file
"""
# can't define as a default parameter because LOGFILE name is potentially
# modified based on SIMULATION flag
if not file:
file = LOGFILE
# special case: for the main logfile, we always keep a rolling log
if not rolling and file == LOGFILE:
rolling = ROLLING_LOGFILE
#if file == LOGFILE:
# lock = filelock.FileLock(LOGFILE_LOCK)
# lock.acquire()
try:
with open(file, 'a') as f:
# by excluding the timestamp, file diffs become easier between runs
if not SIMULATION or file == LOGFILE:
f.write('='*80+'\n')
f.write(str(datetime.datetime.now(TZ))+'\n')
f.write('\n')
f.write(str(message)+'\n')
except IOError:
Log('Unable to append to ' + file)
if rolling:
existing_log_lines = ReadFile(file).splitlines()
with open(rolling, 'w') as f:
f.write('\n'.join(existing_log_lines[-1000:]))
#if file == LOGFILE:
# lock.release()
def MaintainRollingWebLog(message, max_count, filename=None):
"""Maintains a rolling text file of at most max_count printed messages.
Newest data at top and oldest data at the end, of at most max_count messages,
where the delimiter between each message is identified by a special fixed string.
Args:
message: text message to prepend to the file.
max_count: maximum number of messages to keep in the file; the max_count+1st message
is deleted.
filename: the file to update.
"""
# can't define as a default parameter because ROLLING_MESSAGE_FILE name is potentially
# modified based on SIMULATION flag
if not filename:
filename = ROLLING_MESSAGE_FILE
rolling_log_header = '='*(SPLITFLAP_CHARS_PER_LINE + 2)
existing_file = ReadFile(filename)
<----SKIPPED LINES---->
available or if delimiters missing.
"""
settings_dict = {}
for setting in settings.split(';'):
if '=' in setting:
kv_list = setting.split('=')
k = kv_list[0]
v = kv_list[1]
if v.isdigit():
v = int(v)
else:
try:
v = float(v)
except ValueError:
pass
settings_dict[k] = v
return settings_dict
def RemoveSetting(configuration, setting):
"""Removes the named setting from the configuration file."""
configuration.pop(setting)
configuration = BuildSettings(configuration)
WriteFile(CONFIG_FILE, configuration)
return configuration
def WriteFile(filename, text, log_exception=False):
"""Writes the text to the file, returning boolean indicating success.
Args:
filename: string of the filename to open, potentially also including the full path.
text: the text to write
log_exception: boolean indicating whether to log an exception if file not found.
Returns:
Boolean indicating whether the write was successful.
"""
try:
with open(filename, 'w') as content_file:
content_file.write(text)
except IOError:
if log_exception:
Log('Unable to write to '+filename)
return False
return True
<----SKIPPED LINES---->
newly_nearby_flight_numbers = UpdateAircraftList(
persistent_nearby_aircraft, current_nearby_aircraft, now)
if newly_nearby_flight_numbers:
if len(newly_nearby_flight_numbers) > 1:
newly_nearby_flight_numbers_str = ', '.join(newly_nearby_flight_numbers)
newly_nearby_flight_details_str = '\n'.join(
[str(current_nearby_aircraft[f]) for f in newly_nearby_flight_numbers])
Log('Multiple newly-nearby flights: %s\n%s' % (
newly_nearby_flight_numbers_str, newly_nearby_flight_details_str))
flight_number = newly_nearby_flight_numbers[0]
flight_aware_json = {}
if SIMULATION:
json_times = [j[1] for j in FA_JSONS]
if json_time in json_times:
flight_aware_json = FA_JSONS[json_times.index(json_time)][0]
elif flight_number:
flight_aware_json = GetFlightAwareJson(flight_number)
if flight_aware_json:
UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False)
else:
Log('No json returned from Flightaware for flight: %s' % flight_number)
UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True)
flight_details = {}
if flight_aware_json:
flight_details = ParseFlightAwareJson(flight_aware_json)
if not SIMULATION and log_jsons:
PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True)
# Augment FlightAware details with radio / radio-derived details
flight_details.update(current_nearby_aircraft[flight_number])
# Augment with the past location data
flight_details['persistent_path'] = persistent_path[flight_number][1]
return (
persistent_nearby_aircraft,
flight_details,
now,
<----SKIPPED LINES---->
if aircraft.get('squawk') is not None:
simplified_aircraft['squawk'] = aircraft.get('squawk')
track = aircraft.get('track')
if isinstance(track, numbers.Number):
min_meters = MinMetersToHome((lat, lon), track)
simplified_aircraft['track'] = track
simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER
if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
nearby_aircraft[flight_number] = simplified_aircraft
# keep all that track info - once we start reporting on a nearby flight, it will
# become part of the flight's persistent record. Also, note that as we are
# building a list of tracks for each flight, and we are later assigning the
# flight dictionary to point to the list, we just simply need to continue
# updating this list to keep the dictionary up to date (i.e.: we don't need
# to directly touch the flights dictionary in main).
(last_seen, current_path) = persistent_path.get(flight_number, (None, []))
if ( # flight position has been updated with this radio signal
not current_path or
simplified_aircraft.get('lat') != current_path[-1].get('lat') or
simplified_aircraft.get('lon') != current_path[-1].get('lon')):
current_path.append(simplified_aircraft)
persistent_path[flight_number] = (now, current_path)
# if the flight was last seen too far in the past, remove the track info
for flight_number in list(persistent_path.keys()):
(last_seen, current_path) = persistent_path[flight_number]
if last_seen < now - PERSISTENCE_SECONDS:
persistent_path.pop(flight_number)
return (nearby_aircraft, now, json_desc_dict, persistent_path)
def GetFlightAwareJson(flight_number):
"""Scrapes the text json message from FlightAware for a given flight number.
Given a flight number, loads the corresponding FlightAware webpage for that flight and
extracts the relevant script that contains all the flight details from that page.
Args:
flight_number: text flight number (i.e.: SWA1234)
Returns:
Text representation of the json message from FlightAware.
"""
url = 'https://flightaware.com/live/flight/' + flight_number
try:
response = requests.get(url)
except requests.exceptions.RequestException as e:
Log('Unable to query FA for URL due to %s: %s' % (e, url))
return ''
soup = bs4.BeautifulSoup(response.text, 'html.parser')
l = soup.find_all('script')
flight_script = None
for script in l:
if "trackpollBootstrap" in str(script):
flight_script = str(script)
break
if not flight_script:
Log('Unable to find trackpollBootstrap script in page: ' + response.text)
return ''
first_open_curly_brace = flight_script.find('{')
last_close_curly_brace = flight_script.rfind('}')
flight_json = flight_script[first_open_curly_brace:last_close_curly_brace+1]
return flight_json
def Unidecode(s):
"""Convert a special unicode characters to closest ASCII representation."""
if s is not None:
s = unidecode.unidecode(s)
return s
<----SKIPPED LINES---->
message_queue: List of flight messages that have not yet been printed.
flights: List of flights dictionaries.
"""
if flights:
histogram = {
'type': 'both',
'histogram':'all',
'histogram_history':'30d',
'histogram_max_screens': '_2',
'histogram_data_summary': 'on'}
message_queue.extend(TriggerHistograms(flights, histogram))
while message_queue:
ManageMessageQueue(message_queue, 0, {'setting_delay': 0})
SaveFlightsByAltitudeDistanceCSV(flights)
SaveFlightsToCSV(flights)
# repickle to a new .pk with full track info
file_parts = PICKLE_FLIGHTS.split('.')
new_pickle_file = '.'.join([file_parts[0] + '_full_path', file_parts[1]])
RemoveFile(new_pickle_file)
for flight in flights:
PickleObjectToFile(flight, new_pickle_file, False)
print('Simulation complete after %s dump json messages processed' % len(DUMP_JSONS))
def SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft):
"""Slows down simulations when a reported-upon flight is nearby."""
if flights and flights[-1].get('flight_number') in persistent_nearby_aircraft:
time.sleep(arduino.WRITE_DELAY_TIME)
def DumpJsonChanges():
"""Identifies if sequential dump json files changes, for simulation optimization.
If we are logging the radio output faster than it is updating, then there will be
sequential log files in the json list that are identical; we only need to process the
first of these, and can ignore subsequent ones, without any change of output in the
simulation results. This function identifies whether the current active json changed
from the prior one.
Returns:
Boolean - True if different (and processing needed), False if identical
"""
if SIMULATION_COUNTER == 0:
return True
(this_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER]
(last_json, unused_now) = DUMP_JSONS[SIMULATION_COUNTER - 1]
return this_json != last_json
def CheckRebootNeeded(startup_time, message_queue, json_desc_dict, configuration):
"""Reboot based on duration instance has been running.
Reboot needed in one of the following situations:
- All quiet: if running for over 24 hours and all is quiet (message queue empty and
no planes in radio).
- Mostly quiet: if running for over 36 hours and message queue is empty and it's 3a.
- Reboot requested via html form.
Also checks if reset requested via html form.
"""
reboot = False
end_process = False
running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
if (
running_hours >= HOURS_IN_DAY and
not message_queue and
not json_desc_dict.get('radio_range_flights')):
reboot = True
Log('All quiet reboot needed after running for %.2f hours' % running_hours)
if (
running_hours > HOURS_IN_DAY * 1.5 and
not message_queue and
int(EpochDisplayTime(time.time(), '%-H')) >= 3):
reboot = True
Log('Early morning reboot needed after running for %.2f hours' % running_hours)
if 'soft_reboot' in configuration:
reboot = True
Log('Soft reboot requested via web form')
RemoveSetting(configuration, 'soft_reboot')
if 'end_process' in configuration:
Log('Process end requested via web form')
RemoveSetting(configuration, 'end_process')
end_process = True
if reboot or end_process:
global SHUTDOWN_SIGNAL
SHUTDOWN_SIGNAL = True
return reboot
def InterruptRebootFromButton():
"""Sets flag so that the main loop will terminate when it completes the iteration.
This function is only triggered by an physical button press.
"""
global SHUTDOWN_SIGNAL
SHUTDOWN_SIGNAL = True
global REBOOT_SIGNAL
REBOOT_SIGNAL = True
RPi.GPIO.output(GPIO_SOFT_RESET[1], False) # signal that reset received
Log('Soft reboot requested by button push')
def InterruptShutdownFromSignal(signalNumber, unused_frame):
"""Sets flag so that the main loop will terminate when it completes the iteration.
The function signature is defined by the python language - i.e.: these two variables
are passed automatically for registered signals. This function is only triggered by an
interrupt signal.
"""
global SHUTDOWN_SIGNAL
SHUTDOWN_SIGNAL = True
Log('%d received termination signal %d (%s)' % (
os.getpid(), signalNumber,
signal.Signals(signalNumber).name)) # pylint: disable=E1101
def PerformGracefulShutdown(queues, shutdown, reboot):
"""Complete the graceful shutdown process by cleaning up.
Args:
queues: iterable of queues shared with child processes to be closed
shutdown: tuple of shared flags with child processes to initiate shutdown in children
reboot: boolean indicating whether we should trigger a reboot
"""
reboot_msg = ''
if reboot:
reboot_msg = ' and rebooting'
Log('Shutting down self (%d)%s' % (os.getpid(), reboot_msg))
for q in queues:
q.close()
for v in shutdown: # send the shutdown signal to child processes
v.value = 1
if RASPBERRY_PI:
RPi.GPIO.cleanup()
if reboot or REBOOT_SIGNAL:
time.sleep(10) # wait 10 seconds for children to shut down as well
os.system('sudo reboot')
sys.exit()
def FindRunningParents():
"""Returns list of proc ids of processes with identically-named python file running.
In case there are multiple children processes spawned with the same name, such as via
multiprocessing, this will only return the parent id (since a killed child process
will likely just be respawned).
"""
this_process_id = os.getpid()
this_process_name = os.path.basename(sys.argv[0])
pids = []
pid_pairs = []
for proc in psutil.process_iter():
try:
# Check if process name contains this_process_name.
commands = proc.as_dict(attrs=['cmdline', 'pid', 'ppid'])
if commands['cmdline']:
command_running = any(
[this_process_name in s for s in commands['cmdline']])
if command_running:
pids.append(commands['pid'])
pid_pairs.append((commands['pid'], commands['ppid']))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
# Exclude those pids that have a parent that is also a pid
final_pids = []
for pid_pair in pid_pairs:
if pid_pair[1] not in pids:
final_pids.append(pid_pair[0])
# Exclude this pid
final_pids.pop(final_pids.index(this_process_id))
return sorted(final_pids)
def WaitUntilKillComplete(already_running_ids, max_seconds=30):
"""Prevents main loop from starting until other instance, if any, completes shutdown.
A termination command send to any other identically-named process may take a few
seconds to complete because that other process is allowed to finish the current iteration
in the main loop. Typically, that iteration in the other process will complete before
this process finishes the initialization and starts. But in limited scenarios, that might
not happen, such as if the other process is in the middle of generating a lot of
histogram images, or if this process does not have much data to load.
This function ensures that this process does not start the main loop until the other
process terminates. If it detects that the other process is still running, it waits
for up to max_seconds. If the other process does not terminate before that time limit,
then this restarts the RPi.
"""
still_running_ids = FindRunningParents()
if not still_running_ids:
return
# still_running_ids should at least be a subset of already_running_ids
new_processes = sorted(list(set(still_running_ids).difference(set(already_running_ids))))
if new_processes:
# uh-oh! at least one new started up in the interim? exit!
Log('Kill signal sent to %s from this process %s, but it seems like there is '
'at least one new process running, %s!' % (
str(already_running_ids), str(os.getpid()), str(new_processes)))
sys.exit()
# phew - they're a subset; so they probably got the signal; just wait a few secs
elif still_running_ids:
n = 0
running_parents = FindRunningParents()
while running_parents:
if n == max_seconds:
Log('Kill signal sent from this process %d to %s, but %s still '
'running after waiting cume %d seconds; rebooting' % (
os.getpid(), str(already_running_ids), str(running_parents), n+1))
PerformGracefulShutdown((), (), True)
if not n % 3:
Log('Kill signal sent from this process %d to %s, but %s still '
'running after waiting cume %d seconds' % (
os.getpid(), str(already_running_ids), str(running_parents), n))
n += 1
time.sleep(1)
running_parents = FindRunningParents()
def InitArduinos(configuration):
"""Initializes and starts the two arduino threads with new shared-memory queues."""
to_remote_q = multiprocessing.Queue()
to_servo_q = multiprocessing.Queue()
to_main_q = multiprocessing.Queue()
shutdown_remote = multiprocessing.Value('i') # shared flag to initiate shutdown
shutdown_servo = multiprocessing.Value('i') # shared flag to initiate shutdown
shutdown = (shutdown_remote, shutdown_servo)
remote, servo = ValidateArduinosRunning(
None, None, to_remote_q, to_servo_q, to_main_q, shutdown, configuration)
return (remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown)
def RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
<----SKIPPED LINES---->
def ValidateArduinosRunning(
remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration):
"""Ensures that each of the enabled arduinos are running, restarting if needed."""
remote = ValidateSingleRunning(
'enable_remote' in configuration,
arduino.RemoteMain, p=remote,
args=(to_remote_q, to_main_q, shutdown[0]))
servo = ValidateSingleRunning(
'enable_servos' in configuration,
arduino.ServoMain, p=servo,
args=(to_servo_q, to_main_q, shutdown[1]))
return remote, servo
def ValidateSingleRunning(enabled, start_function, p=None, args=()):
"""Restarts a new instance of multiprocessing process if not running"""
if not SHUTDOWN_SIGNAL:
if not enabled:
if p is not None: # must have just requested a disabling of single instance
args[2].value = 1 # trigger a shutdown on the single instance
return None
if p is None or not p.is_alive():
if p is None:
Log('Process for %s starting for first time' % str(start_function))
else:
Log('Process (%s) for %s died; restarting' % (str(p), str(start_function)))
args[2].value = 0 # (re)set shutdown flag to allow function to run
p = multiprocessing.Process(target=start_function, args=args)
p.daemon = True
p.start()
return p
def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q):
"""Send latest data to arduinos via their shared-memory queues"""
last_flight = {}
if flights:
last_flight = dict(flights[-1])
if SIMULATION:
now = json_desc_dict['now']
else:
now = time.time()
<----SKIPPED LINES---->
messageboard_flight_index = IdentifyFlightDisplayed(
flights, configuration, display_all_hours=True)
if messageboard_flight_index is not None:
message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
message_queue = [(FLAG_MSG_FLIGHT, flight_message)]
next_message_time = time.time()
elif command == 'histogram':
if not flights:
Log('Histogram requested by remote %s but no flights in memory' % str(args))
else:
histogram_type, histogram_history = args
message_queue.extend(MessageboardHistograms(
flights,
histogram_type,
histogram_history,
'_1',
False))
elif command == 'update_configuration':
WriteFile(CONFIG_FILE, *args)
else:
Log('Improper command from arduinos: %s / %s' % (command, args))
return message_queue, next_message_time
def PublishMessage(
s,
subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
timeout=3):
"""Publishes a text string to a Vestaboard.
The message is pushed to the vestaboard splitflap display by way of its web services;
see https://docs.vestaboard.com/introduction for more details.
Args:
<----SKIPPED LINES---->
'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id)
curl.setopt(pycurl.HTTPHEADER, [
'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret])
curl.setopt(pycurl.TIMEOUT_MS, timeout*1000)
curl.setopt(pycurl.POST, 1)
curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean
# preparing body the way pycurl.READDATA wants it
body_as_dict = {'text': s}
body_as_json_string = json.dumps(body_as_dict) # dict to json
body_as_file_object = io.StringIO(body_as_json_string)
# prepare and send. See also: pycurl.READFUNCTION to pass function instead
curl.setopt(pycurl.READDATA, body_as_file_object)
curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string))
try:
curl.perform()
except pycurl.error as e:
Log('curl.perform() failed with message %s' % e)
error_code = True
else:
# you may want to check HTTP response code, e.g.
status_code = curl.getinfo(pycurl.RESPONSE_CODE)
if status_code != 200:
Log('Server returned HTTP status code %d for message %s' % (status_code, s))
error_code = True
curl.close()
UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, error_code)
def ManageMessageQueue(message_queue, next_message_time, configuration):
"""Check time & if appropriate, display next message from queue.
Args:
message_queue: FIFO list of message tuples of (message type, message string).
next_message_time: epoch at which next message should be displayed
configuration: dictionary of configuration attributes.
Returns:
Next_message_time, potentially updated if a message has been displayed, or unchanged
if no message was displayed.
"""
if message_queue and (time.time() >= next_message_time or SIMULATION):
if SIMULATION: # drain the queue because the messages come so fast
messages_to_display = list(message_queue)
# passed by reference, so clear it out since we drained it to the display
del message_queue[:]
<----SKIPPED LINES---->
be considered correct.
They are "correct" in the sense that that new insight was not available at the time
that older flight was seen, but it is not correct in the sense that, because this new
insight is starting out with an incidence in the historical data of zero, this
new insight may be reported more frequently than desired until it "catches up".
So this method replays the flight history with the latest insight code, regenerating
the insight distribution for each flight.
"""
directory, file = os.path.split(full_path)
all_files = os.listdir(directory)
files = sorted([os.path.join(directory, f) for f in all_files if file in f])
for f in files:
print('Bootstrapping %s' % f)
configuration = ReadAndParseSettings(CONFIG_FILE)
flights = []
tmp_f = f + 'tmp'
RemoveFile(tmp_f)
if os.path.exists(f):
mtime = os.path.getmtime(f)
flights = UnpickleObjectFromFile(f, False)
for (n, flight) in enumerate(flights):
if n/25 == int(n/25):
print(' - %d' % n)
CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
PickleObjectToFile(flight, tmp_f, False)
if mtime == os.path.getmtime(f):
shutil.move(tmp_f, f)
else:
print('Aborted: failed to bootstrap %s: file changed while in process' % full_path)
return
def ResetLogs(config):
"""Clears the non-scrolling logs if reset_logs in config."""
if 'reset_logs' in config:
Log('Reset logs')
for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE):
if RemoveFile(f):
open(f, 'a').close()
config.pop('reset_logs')
config = BuildSettings(config)
WriteFile(CONFIG_FILE, config)
return config
def CheckTemperature(fan_power):
"""Turn on fan if temperature exceeds threshold."""
if RASPBERRY_PI:
temperature = gpiozero.CPUTemperature().temperature
if temperature > TEMP_FAN_TURN_ON_CELSIUS and not fan_power:
fan_power = True
RPi.GPIO.output(GPIO_FAN, True)
Log('Fan turned on at temperature %.1f' % temperature)
elif temperature < TEMP_FAN_TURN_OFF_CELSIUS and fan_power:
fan_power = False
RPi.GPIO.output(GPIO_FAN, False)
Log('Fan turned off at temperature %.1f' % temperature)
return fan_power
pin_values = {} # caches last set value
def SetPinMode():
"""Initialize output GPIO pins for output on Raspberry Pi."""
global pin_values
if RASPBERRY_PI:
RPi.GPIO.setmode(RPi.GPIO.BCM)
for pin in (
GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION,
GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2):
# Initialize some pins to start in error condition
if pin in (
GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION,
GPIO_UNUSED_1, GPIO_UNUSED_2):
pin_values[pin[0]] = True
else:
pin_values[pin[0]] = False
if RASPBERRY_PI:
RPi.GPIO.setup(pin[0], RPi.GPIO.OUT)
RPi.GPIO.output(pin[0], pin_values[pin[0]])
if RASPBERRY_PI: # configure soft reset button
RPi.GPIO.setup(GPIO_SOFT_RESET[0], RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN)
RPi.GPIO.setup(GPIO_SOFT_RESET[1], RPi.GPIO.OUT)
RPi.GPIO.output(GPIO_SOFT_RESET[1], True)
RPi.GPIO.add_event_detect(GPIO_SOFT_RESET[0], RPi.GPIO.RISING)
RPi.GPIO.add_event_callback(GPIO_SOFT_RESET[0], InterruptRebootFromButton)
def UpdateStatusLight(pin, value):
"""Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value."""
global pin_values
if value:
msg = pin[1]
else:
msg = pin[2]
if RASPBERRY_PI:
RPi.GPIO.output(pin[0], value)
if value:
pin_setting = 'HIGH'
relay_light_value = 'OFF'
else:
pin_setting = 'LOW'
relay_light_value = 'ON'
msg += '; RPi GPIO pin %d set to %s; relay light #%d should now be %s' % (
pin[0], pin_setting, pin[3], relay_light_value)
if pin_values[pin[0]] != value:
Log(msg)
pin_values[pin[0]] = value
def RemoveFile(file):
"""Removes a file if it exists, returning a boolean indicating if it had existed."""
if os.path.exists(file):
os.remove(file)
return True
return False
def main():
"""Traffic cop between incoming radio flight messages, configuration, and messageboard.
This is the main logic, checking for new flights, augmenting the radio signal with
additional web-scraped data, and generating messages in a form presentable to the
messageboard.
"""
RemoveFile(LOGFILE_LOCK)
# Since this clears log files, it should occur first before we start logging
if '-s' in sys.argv:
global SIMULATION_COUNTER
SimulationSetup()
# This flag slows down simulation time around a flight, great for debugging the arduinos
simulation_slowdown = bool('-f' in sys.argv)
# Redirect any errors to a log file instead of the screen, and add a datestamp
if not SIMULATION:
sys.stderr = open(STDERR_FILE, 'a')
Log('', STDERR_FILE)
Log('Starting up process %d' % os.getpid())
already_running_ids = FindRunningParents()
if already_running_ids:
for pid in already_running_ids:
Log('Sending termination signal to %d' % pid)
os.kill(pid, signal.SIGTERM)
SetPinMode()
fan_power = False
configuration = ReadAndParseSettings(CONFIG_FILE)
startup_time = time.time()
json_desc_dict = {}
flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS)
# Clear the loaded flight of any cached data, identified by keys with a specific
# suffix, since code fixes may change the values for some of those cached elements
for flight in flights:
for key in list(flight.keys()):
if key.endswith(CACHED_ELEMENT_PREFIX):
flight.pop(key)
# If we're displaying just a single insight message, we want it to be something
# unique, to the extent possible; this dict holds a count of the diff types of messages
# displayed so far
insight_message_distribution = {}
# bootstrap the flight insights distribution from a list of insights on each
<----SKIPPED LINES---->
configuration.get('log_jsons', False))
if flight:
flights.append(flight)
remote, servo = RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
flights, json_desc_dict, configuration)
flight_meets_display_criteria = FlightMeetsDisplayCriteria(
flight, configuration, log=True)
if flight_meets_display_criteria:
flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight))
# display the next message about this flight now!
next_message_time = time.time()
message_queue.insert(0, flight_message)
# and delete any queued insight messages about other flights that have
# not yet displayed, since a newer flight has taken precedence
messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INTERESTING]
if messages_to_delete and VERBOSE:
Log(
'Deleting messages from queue due to new-found plane: %s' %
messages_to_delete)
message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
# Though we also manage the message queue outside this conditional as well,
# because it can take a half second to generate the flight insights, this allows
# this message to start displaying on the board immediately, so it's up there
# when it's most relevant
next_message_time = ManageMessageQueue(
message_queue, next_message_time, configuration)
insight_messages = CreateFlightInsights(
flights, configuration.get('insights'), insight_message_distribution)
if configuration.get('next_flight', 'off') == 'on':
next_flight_text = FlightInsightNextFlight(flights, configuration)
if next_flight_text:
insight_messages.insert(0, next_flight_text)
insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages]
<----SKIPPED LINES---->
PickleObjectToFile(flight, PICKLE_FLIGHTS, not SIMULATION)
else:
remote, servo = RefreshArduinos(
remote, servo,
to_remote_q, to_servo_q, to_main_q, shutdown,
flights, json_desc_dict, configuration)
message_queue, next_message_time = ProcessArduinoCommmands(
to_main_q, flights, configuration, message_queue, next_message_time)
if SIMULATION:
if now:
simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z')
if simulated_hour != prev_simulated_hour:
print(simulated_hour)
prev_simulated_hour = simulated_hour
histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
RemoveFile(HISTOGRAM_CONFIG_FILE)
# We also need to make sure there are flights on which to generate a histogram! Why
# might there not be any flights? Primarily during a simulation, if there's a
# lingering histogram file at the time of history restart.
if histogram and not flights:
Log('Histogram requested (%s) but no flights in memory' % histogram)
if histogram and flights:
message_queue.extend(TriggerHistograms(flights, histogram))
# check time & if appropriate, display next message from queue
next_message_time = ManageMessageQueue(message_queue, next_message_time, configuration)
reboot = CheckRebootNeeded(startup_time, message_queue, json_desc_dict, configuration)
fan_power = CheckTemperature(fan_power)
if not SIMULATION:
time.sleep(max(0, next_loop_time - time.time()))
next_loop_time = time.time() + LOOP_DELAY_SECONDS
else:
SIMULATION_COUNTER += 1
if simulation_slowdown:
SimulationSlowdownNearFlight(flights, persistent_nearby_aircraft)
if SHUTDOWN_SIGNAL: # do a graceful exit
PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)
if SIMULATION:
SimulationEnd(message_queue, flights)
PerformGracefulShutdown((to_remote_q, to_servo_q, to_main_q), shutdown, reboot)
if __name__ == "__main__":
#interrupt, as in ctrl-c
signal.signal(signal.SIGINT, InterruptShutdownFromSignal)
#terminate, when another instance found or via kill
signal.signal(signal.SIGTERM, InterruptShutdownFromSignal)
if '-i' in sys.argv:
BootstrapInsightList()
else:
main()
|