01234567890123456789012345678901234567890123456789012345678901234567890123456789
7273747576777879808182838485868788899091929394 9596979899100101102103104105106107108109110111112113114 227228229230231232233234235236237238239240241242243244245246 247248249250251252253254255256257258259260261262263264265266 13911392139313941395139613971398139914001401140214031404140514061407140814091410 14111412141314141415141614171418141914201421142214231424142514261427142814291430 5253525452555256525752585259526052615262526352645265526652675268526952705271527252735274 52755276 527752785279528052815282528352845285528652875288528952905291529252935294529552965297 53275328532953305331533253335334533553365337533853395340534153425343534453455346 53475348 5349535053515352535353545355535653575358535953605361536253635364 53655366536753685369537053715372537353745375537653775378537953805381538253835384 5395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437 55985599560056015602560356045605560656075608560956105611561256135614561556165617 5618561956205621562256235624 56255626562756285629563056315632563356345635563656375638563956405641564256435644 59505951595259535954595559565957595859595960596159625963596459655966596759685969 59705971597259735974597559765977597859795980598159825983598459855986598759885989 60146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153 | <----SKIPPED LINES----> # number of seconds to wait between recording heartbeats to the status file HEARTBEAT_SECONDS = 10 # version control directory CODE_REPOSITORY = '' VERSION_REPOSITORY = 'versions/' VERSION_WEBSITE_PATH = VERSION_REPOSITORY VERSION_MESSAGEBOARD = None VERSION_ARDUINO = None MAX_INSIGHT_HORIZON_DAYS = 31 # histogram logic truncates to exactly 30 days of hours # This file is where the radio drops its json file DUMP_JSON_FILE = '/run/readsb/aircraft.json' # At the time a flight is first identified as being of interest (in that it falls # within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware, # if any - is appended to the end of this pickle file. However, since this file is # cached in working memory, flights older than 30 days are flushed from this periodically. PICKLE_FLIGHTS = 'pickle/flights.pk' # True splits all the flights created in simulation into separate date files, just like # the non-simulated runs; False consolidates all flights into one pickle file. SPLIT_SIMULATION_FLIGHT_PICKLE = False # Status data about messageboard - is it running, etc. Specifically, has tuples # of data (timestamp, system_id, status), where system_id is either the pin id of GPIO, # or a 0 to indicate overall system, and status is boolean PICKLE_DASHBOARD = 'pickle/dashboard.pk' CACHED_ELEMENT_PREFIX = 'cached_' # This web-exposed file is used for non-error messages that might highlight data or # code logic to check into. It is only cleared out manually. LOGFILE = 'log.txt' # Identical to the LOGFILE, except it includes just the most recent n lines. Newest # lines are at the end. ROLLING_LOGFILE = 'rolling_log.txt' #file for error messages ROLLING_LOG_SIZE = 1000 # default number of lines which may be overridden by settings file # Users can trigger .png histograms analogous to the text ones from the web interface; # this is the folder (within WEBSERVER_PATH) where those files are placed WEBSERVER_IMAGE_RELATIVE_FOLDER = 'images/' <----SKIPPED LINES----> 27, 'Undefined condition set to true', 'Undefined condition set to false', 6, 'Unused', False) GPIO_UNUSED_2 = ( 6, 'Undefined condition set to true', 'Undefined condition set to false', 8, 'Unused', False) # GPIO pushbutton connections - (GPIO pin switch in; GPIO pin LED out) GPIO_SOFT_RESET = (20, 21) #if running on raspberry, then need to prepend path to file names if RASPBERRY_PI: PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_DASHBOARD LOGFILE = MESSAGEBOARD_PATH + LOGFILE PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE CODE_REPOSITORY = MESSAGEBOARD_PATH HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE STDERR_FILE = WEBSERVER_PATH + STDERR_FILE BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE UPTIMES_FILE = WEBSERVER_PATH + UPTIMES_FILE HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY TIMEZONE = 'US/Pacific' # timezone of display TZ = pytz.timezone(TIMEZONE) <----SKIPPED LINES----> parsed: The parsed json file. Returns: Dictionary with attributes about radio range, number of flights seen, etc. """ json_desc_dict = {} json_desc_dict['now'] = parsed['now'] aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS] json_desc_dict['radio_range_flights'] = len(aircraft) aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a] current_distances = [HaversineDistanceMeters( HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos] current_distances = [ d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None] if current_distances: json_desc_dict['radio_range_miles'] = max(current_distances) return json_desc_dict def MergedIdentifier(proposed_id, existing_ids): """Identifies what identifier to use for a flight. While most flights have both a squawk and a flight number, enough are missing one only for it to appear later to want to use a 2-tuple of both as an identifier, merging flights if they share a common non-null flight number and/or squawk, as the persistent identifier across time. Additionally, in very limited circumstances, a squawk may change mid-flight; in that case, the first alpha squawk is used. This function identifies which identifier to use, and which - if any - should be merged into that one identifier from a group of existing identifiers. Args: proposed_id: The 2-tuple of (flight_number, squawk) of the identified flight. existing_ids: An iterable of existing 2-tuple identifiers, some (or none) of which may overlap with this flight. <----SKIPPED LINES----> n += 1 time.sleep(1) running_parents = FindRunningParents() def InitArduinoVariables(): """Initializes and starts the two arduino threads with new shared-memory queues.""" to_remote_q = multiprocessing.Queue() to_servo_q = multiprocessing.Queue() to_main_q = multiprocessing.Queue() shutdown_remote = multiprocessing.Value('i') # shared flag to initiate shutdown shutdown_servo = multiprocessing.Value('i') # shared flag to initiate shutdown shutdown = (shutdown_remote, shutdown_servo) return (to_remote_q, to_servo_q, to_main_q, shutdown) def RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration): """Ensure arduinos are running, restarting if needed, & send them the current message""" remote, servo = ValidateArduinosRunning( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration) EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q) return remote, servo def ValidateArduinosRunning( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration): """Ensures that each of the enabled arduinos are running, restarting if needed. Args: remote: Running remote Arduino process (or if not previously running, None value) servo: Running servo Arduino process (or if not previously running, None value) to_remote_q: Multi-processing messaging queue for one-way comm from messageboard to remote arduino. to_servo_q: Multi-processing messaging queue for one-way comm from messageboard to servo arduino. to_main_q: Multi-processing messaging queue for one-way comm from arduinos to messageboard. shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective arduinos when they should be shutdown. configuration: Dictionary of configuration settings. <----SKIPPED LINES----> if not SHUTDOWN_SIGNAL: if not enabled: if p is not None: # must have just requested a disabling of single instance args[2].value = 1 # trigger a shutdown on the single instance return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) elif VERBOSE: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = False # TODO: perhaps value of false will address correlated BT failures? p.start() return p def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q): """Send latest data to arduinos via their shared-memory queues""" last_flight = {} if flights: last_flight = flights[-1] if SIMULATION: now = json_desc_dict['now'] else: now = time.time() additional_attributes = {} today = EpochDisplayTime(now, '%x') flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today]) additional_attributes['flight_count_today'] = flight_count_today additional_attributes['simulation'] = SIMULATION message = (last_flight, json_desc_dict, configuration, additional_attributes) try: if 'enable_servos' in configuration: to_servo_q.put(message, block=False) if 'enable_remote' in configuration: to_remote_q.put(message, block=False) except queue.Full: msg = 'Message queues to Arduinos full - trigger shutdown' Log(msg) global SHUTDOWN_SIGNAL SHUTDOWN_SIGNAL = msg def ProcessArduinoCommmands(q, flights, configuration, message_queue, next_message_time): """Executes the commands enqueued by the arduinos. The commands on the queue q are of the form (command, args), where command is an identifier indicating the type of instruction, and the args is a possibly empty tuple with the attributes to follow thru. <----SKIPPED LINES----> Returns: A 2-tuple of the (possibly-updated) message_queue and next_message_time. """ while not q.empty(): command, args = q.get() if command == 'pin': UpdateStatusLight(*args) elif command == 'replay': # a command might request info about flight to be (re)displayed, irrespective of # whether the screen is on; if so, let's put that message at the front of the message # queue, and delete any subsequent messages in queue because presumably the button # was pushed either a) when the screen was off (so no messages in queue), or b) # because the screen was on, but the last flight details got lost after other screens # that we're no longer interested in messageboard_flight_index = IdentifyFlightDisplayed( flights, configuration, display_all_hours=True) if messageboard_flight_index is not None: message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INSIGHT] flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index]) message_queue = [(FLAG_MSG_FLIGHT, flight_message)] next_message_time = time.time() elif command == 'histogram': if not flights: Log('Histogram requested by remote %s but no flights in memory' % str(args)) else: histogram_type, histogram_history = args message_queue.extend(MessageboardHistograms( flights, histogram_type, histogram_history, '_1', False)) elif command == 'update_configuration': updated_settings = args[0] Log('Updated settings received from arduino: %s' % updated_settings) WriteFile(CONFIG_FILE, updated_settings) else: <----SKIPPED LINES----> Returns: Next_message_time, potentially updated if a message has been displayed, or unchanged if no message was displayed. """ if message_queue and (time.time() >= next_message_time or SIMULATION): if SIMULATION: # drain the queue because the messages come so fast messages_to_display = list(message_queue) # passed by reference, so clear it out since we drained it to the display del message_queue[:] else: # display only one message, being mindful of the display timing messages_to_display = [message_queue.pop(0)] for message in messages_to_display: message_text = message[1] if isinstance(message_text, str): message_text = textwrap.wrap(message_text, width=SPLITFLAP_CHARS_PER_LINE) display_message = Screenify(message_text, False) Log(display_message, file=ALL_MESSAGE_FILE) MaintainRollingWebLog(display_message, 25) if not SIMULATION: splitflap_message = Screenify(message_text, True) PublishMessage(splitflap_message) next_message_time = time.time() + configuration['setting_delay'] return next_message_time def BootstrapInsightList(full_path=PICKLE_FLIGHTS): """(Re)populate flight pickle files with flight insight distributions. The set of insights generated for each flight is created at the time the flight was first identified, and saved on the flight pickle. This saving allows the current running distribution to be recalculated very quickly, but it means that as code enabling new insights gets added, those historical distributions may not necessarily be considered correct. They are "correct" in the sense that that new insight was not available at the time that older flight was seen, but it is not correct in the sense that, because this new insight is starting out with an incidence in the historical data of zero, this new insight may be reported more frequently than desired until it "catches up". So this method replays the flight history with the latest insight code, regenerating the insight distribution for each flight. """ directory, file = os.path.split(full_path) <----SKIPPED LINES----> os.kill(pid, signal.SIGTERM) init_timing.append((time.time(), 2)) SetPinMode() configuration = ReadAndParseSettings(CONFIG_FILE) Log('Read CONFIG_FILE at %s: %s' % (CONFIG_FILE, str(configuration))) startup_time = time.time() json_desc_dict = {} init_timing.append((time.time(), 3)) flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS) # Clear the loaded flight of any cached data, identified by keys with a specific # suffix, since code fixes may change the values for some of those cached elements for flight in flights: for key in list(flight.keys()): if key.endswith(CACHED_ELEMENT_PREFIX): flight.pop(key) init_timing.append((time.time(), 4)) # If we're displaying just a single insight message, we want it to be something # unique, to the extent possible; this dict holds a count of the diff types of messages # displayed so far insight_message_distribution = {} # bootstrap the flight insights distribution from a list of insights on each # flight (i.e.: flight['insight_types'] for a given flight might look like # [1, 2, 7, 9], or [], to indicate which insights were identified; this then # transforms that into {0: 25, 1: 18, ...} summing across all flights. missing_insights = [] for flight in flights: if 'insight_types' not in flight: missing_insights.append( '%s on %s' % (DisplayFlightNumber(flight), DisplayTime(flight, '%x %X'))) distribution = flight.get('insight_types', []) for key in distribution: insight_message_distribution[key] = ( insight_message_distribution.get(key, 0) + 1) if missing_insights: <----SKIPPED LINES----> next_loop_time = time.time() + LOOP_DELAY_SECONDS # These files are read only if the version on disk has been modified more recently # than the last time it was read last_dump_json_timestamp = 0 init_timing.append((time.time(), 6)) WaitUntilKillComplete(already_running_ids) init_timing.append((time.time(), 7)) LogTimes(init_timing) Log('Finishing initialization of %d; starting radio polling loop' % os.getpid()) while (not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS)) and not SHUTDOWN_SIGNAL: last_heartbeat_time = Heartbeat(last_heartbeat_time) new_configuration = ReadAndParseSettings(CONFIG_FILE) UpdateRollingLogSize(new_configuration) CheckForNewFilterCriteria(configuration, new_configuration, message_queue, flights) if 'setting_screen_enabled' not in configuration and 'setting_screen_enabled' in new_configuration: Log('setting_screen_enabled changed from NOT PRESENT to ON') if 'setting_screen_enabled' in configuration and 'setting_screen_enabled' not in new_configuration: Log('setting_screen_enabled changed from ON to NOT PRESENT') last_configuration = configuration configuration = new_configuration ResetLogs(configuration) # clear the logs if requested UpdateRollingLogSize(configuration) # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation, # then only read & do related processing for the next dump if the last-modified # timestamp indicates the file has been updated since it was last read. tmp_timestamp = 0 if not SIMULATION: dump_json_exists = os.path.exists(DUMP_JSON_FILE) if dump_json_exists: tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE) if (SIMULATION and DumpJsonChanges()) or ( not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp): last_dump_json_timestamp = tmp_timestamp (persistent_nearby_aircraft, flight, now, json_desc_dict, persistent_path) = ScanForNewFlights( persistent_nearby_aircraft, persistent_path, configuration.get('log_jsons', False)) # because this might just be an updated instance of the previous flight as more # identifier information (squawk and or flight number) comes in, we only want to # process this if its a truly new flight new_flight_flag = ConfirmNewFlight(flight, flights) if new_flight_flag: flights.append(flight) remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration) flight_meets_display_criteria = FlightMeetsDisplayCriteria( flight, configuration, log=True) if flight_meets_display_criteria: flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight)) # display the next message about this flight now! next_message_time = time.time() message_queue.insert(0, flight_message) # and delete any queued insight messages about other flights that have # not yet displayed, since a newer flight has taken precedence messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INSIGHT] if messages_to_delete and VERBOSE: Log( 'Deleting messages from queue due to new-found plane: %s' % messages_to_delete) message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INSIGHT] # Though we also manage the message queue outside this conditional as well, # because it can take a half second to generate the flight insights, this allows # this message to start displaying on the board immediately, so it's up there # when it's most relevant next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration) insight_messages = CreateFlightInsights( flights, configuration.get('insights'), insight_message_distribution) if configuration.get('next_flight', 'off') == 'on': next_flight_text = FlightInsightNextFlight(flights, configuration) if next_flight_text: insight_messages.insert(0, next_flight_text) insight_messages = [(FLAG_MSG_INSIGHT, m) for m in insight_messages] for insight_message in insight_messages: message_queue.insert(0, insight_message) else: # flight didn't meet display criteria flight['insight_types'] = [] PickleObjectToFile(flight, PICKLE_FLIGHTS, True, timestamp=flight['now']) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) PersonalMessage(configuration, message_queue) if SIMULATION: if now: simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z') if simulated_hour != prev_simulated_hour: print(simulated_hour) prev_simulated_hour = simulated_hour histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE) RemoveFile(HISTOGRAM_CONFIG_FILE) # We also need to make sure there are flights on which to generate a histogram! Why # might there not be any flights? Primarily during a simulation, if there's a # lingering histogram file at the time of history restart. if histogram and not flights: <----SKIPPED LINES----> |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
72737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 1395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443 526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331 536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447 5458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500 5661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721 602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068 60936094609560966097609860996100610161026103610461056106610761086109611061116112 6113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161 61626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211 | <----SKIPPED LINES----> # number of seconds to wait between recording heartbeats to the status file HEARTBEAT_SECONDS = 10 # version control directory CODE_REPOSITORY = '' VERSION_REPOSITORY = 'versions/' VERSION_WEBSITE_PATH = VERSION_REPOSITORY VERSION_MESSAGEBOARD = None VERSION_ARDUINO = None MAX_INSIGHT_HORIZON_DAYS = 31 # histogram logic truncates to exactly 30 days of hours # This file is where the radio drops its json file DUMP_JSON_FILE = '/run/readsb/aircraft.json' # At the time a flight is first identified as being of interest (in that it falls # within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware, # if any - is appended to the end of this pickle file. However, since this file is # cached in working memory, flights older than 30 days are flushed from this periodically. PICKLE_FLIGHTS = 'pickle/flights.pk' # This allows us to identify the full history (including what was last sent to) the # splitflap display in a programmatic fashion. While it may be interesting in its own # right, its real use is to handle the "replay" button, so we know to enable it if what # is displayed is the last flight. PICKLE_SCREENS = 'pickle/screens.pk' # Status data about messageboard - is it running, etc. Specifically, has tuples # of data (timestamp, system_id, status), where system_id is either the pin id of GPIO, # or a 0 to indicate overall system, and status is boolean PICKLE_DASHBOARD = 'pickle/dashboard.pk' CACHED_ELEMENT_PREFIX = 'cached_' # This web-exposed file is used for non-error messages that might highlight data or # code logic to check into. It is only cleared out manually. LOGFILE = 'log.txt' # Identical to the LOGFILE, except it includes just the most recent n lines. Newest # lines are at the end. ROLLING_LOGFILE = 'rolling_log.txt' #file for error messages ROLLING_LOG_SIZE = 1000 # default number of lines which may be overridden by settings file # Users can trigger .png histograms analogous to the text ones from the web interface; # this is the folder (within WEBSERVER_PATH) where those files are placed WEBSERVER_IMAGE_RELATIVE_FOLDER = 'images/' <----SKIPPED LINES----> 27, 'Undefined condition set to true', 'Undefined condition set to false', 6, 'Unused', False) GPIO_UNUSED_2 = ( 6, 'Undefined condition set to true', 'Undefined condition set to false', 8, 'Unused', False) # GPIO pushbutton connections - (GPIO pin switch in; GPIO pin LED out) GPIO_SOFT_RESET = (20, 21) #if running on raspberry, then need to prepend path to file names if RASPBERRY_PI: PICKLE_FLIGHTS = MESSAGEBOARD_PATH + PICKLE_FLIGHTS PICKLE_DASHBOARD = MESSAGEBOARD_PATH + PICKLE_DASHBOARD LOGFILE = MESSAGEBOARD_PATH + LOGFILE PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE PICKLE_SCREENS = MESSAGEBOARD_PATH + PICKLE_SCREENS CODE_REPOSITORY = MESSAGEBOARD_PATH HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE STDERR_FILE = WEBSERVER_PATH + STDERR_FILE BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE UPTIMES_FILE = WEBSERVER_PATH + UPTIMES_FILE HISTOGRAM_IMAGE_HTML = WEBSERVER_PATH + HISTOGRAM_IMAGE_HTML HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_RELATIVE_FOLDER + HOURLY_IMAGE_FILE VERSION_REPOSITORY = WEBSERVER_PATH + VERSION_REPOSITORY TIMEZONE = 'US/Pacific' # timezone of display TZ = pytz.timezone(TIMEZONE) <----SKIPPED LINES----> parsed: The parsed json file. Returns: Dictionary with attributes about radio range, number of flights seen, etc. """ json_desc_dict = {} json_desc_dict['now'] = parsed['now'] aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS] json_desc_dict['radio_range_flights'] = len(aircraft) aircraft_with_pos = [a for a in aircraft if 'lat' in a and 'lon' in a] current_distances = [HaversineDistanceMeters( HOME, (a['lat'], a['lon'])) for a in aircraft_with_pos] current_distances = [ d * FEET_IN_METER / FEET_IN_MILE for d in current_distances if d is not None] if current_distances: json_desc_dict['radio_range_miles'] = max(current_distances) return json_desc_dict def SameFlight(f1, f2): """True if these two flights are likely the same flight, False otherwise.""" if f1['flight_number'] == f2['flight_number']: return True if f1['squawk'] == f2['squawk']: return True return False def MergedIdentifier(proposed_id, existing_ids): """Identifies what identifier to use for a flight. While most flights have both a squawk and a flight number, enough are missing one only for it to appear later to want to use a 2-tuple of both as an identifier, merging flights if they share a common non-null flight number and/or squawk, as the persistent identifier across time. Additionally, in very limited circumstances, a squawk may change mid-flight; in that case, the first alpha squawk is used. This function identifies which identifier to use, and which - if any - should be merged into that one identifier from a group of existing identifiers. Args: proposed_id: The 2-tuple of (flight_number, squawk) of the identified flight. existing_ids: An iterable of existing 2-tuple identifiers, some (or none) of which may overlap with this flight. <----SKIPPED LINES----> n += 1 time.sleep(1) running_parents = FindRunningParents() def InitArduinoVariables(): """Initializes and starts the two arduino threads with new shared-memory queues.""" to_remote_q = multiprocessing.Queue() to_servo_q = multiprocessing.Queue() to_main_q = multiprocessing.Queue() shutdown_remote = multiprocessing.Value('i') # shared flag to initiate shutdown shutdown_servo = multiprocessing.Value('i') # shared flag to initiate shutdown shutdown = (shutdown_remote, shutdown_servo) return (to_remote_q, to_servo_q, to_main_q, shutdown) def RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration, screen_history): """Ensure arduinos are running, restarting if needed, & send them the current message. Args: remote: Running remote Arduino process (or if not previously running, None value) servo: Running servo Arduino process (or if not previously running, None value) to_remote_q: Multi-processing messaging queue for one-way comm from messageboard to remote arduino. to_servo_q: Multi-processing messaging queue for one-way comm from messageboard to servo arduino. to_main_q: Multi-processing messaging queue for one-way comm from arduinos to messageboard. shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective arduinos when they should be shutdown. flights: List of all flights. json_desc_dict: Dictionary of additional attributes about radio. configuration: Dictionary of configuration settings. screen_history: List of past screens displayed to splitflap screen. Returns: A 2-tuple of the remote and servo running processes. """ remote, servo = ValidateArduinosRunning( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration) EnqueueArduinos( flights, json_desc_dict, configuration, to_servo_q, to_remote_q, screen_history) return remote, servo def ValidateArduinosRunning( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, configuration): """Ensures that each of the enabled arduinos are running, restarting if needed. Args: remote: Running remote Arduino process (or if not previously running, None value) servo: Running servo Arduino process (or if not previously running, None value) to_remote_q: Multi-processing messaging queue for one-way comm from messageboard to remote arduino. to_servo_q: Multi-processing messaging queue for one-way comm from messageboard to servo arduino. to_main_q: Multi-processing messaging queue for one-way comm from arduinos to messageboard. shutdown: 2-tuple of multiprocessing flags (integers) used to signal to respective arduinos when they should be shutdown. configuration: Dictionary of configuration settings. <----SKIPPED LINES----> if not SHUTDOWN_SIGNAL: if not enabled: if p is not None: # must have just requested a disabling of single instance args[2].value = 1 # trigger a shutdown on the single instance return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) elif VERBOSE: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = False # TODO: perhaps value of false will address correlated BT failures? p.start() return p def LastFlightAvailable(flights, screen_history): """Returns True if last message sent to splitflap is not the last flight; else False.""" if not screen_history: return False last_message_tuple = screen_history[-1] last_message_type = last_message_tuple[0] if last_message_type == FLAG_MSG_FLIGHT: last_message_flight = last_message_tuple[2] if SameFlight(last_message_flight, flights[-1]): return False # already displaying the last flight! return True def EnqueueArduinos( flights, json_desc_dict, configuration, to_servo_q, to_remote_q, screen_history): """Send latest data to arduinos via their shared-memory queues. Args: flights: List of all flights. json_desc_dict: Dictionary of additional attributes about radio. configuration: Dictionary of configuration settings. to_servo_q: Multi-processing messaging queue for one-way comm from messageboard to servo arduino. to_remote_q: Multi-processing messaging queue for one-way comm from messageboard to remote arduino. screen_history: List of past screens displayed to splitflap screen. """ last_flight = {} if flights: last_flight = flights[-1] if SIMULATION: now = json_desc_dict['now'] else: now = time.time() additional_attributes = {} today = EpochDisplayTime(now, '%x') flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today]) additional_attributes['flight_count_today'] = flight_count_today additional_attributes['simulation'] = SIMULATION additional_attributes['last_flight_available'] = LastFlightAvailable( flights, screen_history) message = (last_flight, json_desc_dict, configuration, additional_attributes) try: if 'enable_servos' in configuration: to_servo_q.put(message, block=False) if 'enable_remote' in configuration: to_remote_q.put(message, block=False) except queue.Full: msg = 'Message queues to Arduinos full - trigger shutdown' Log(msg) global SHUTDOWN_SIGNAL SHUTDOWN_SIGNAL = msg def ProcessArduinoCommmands(q, flights, configuration, message_queue, next_message_time): """Executes the commands enqueued by the arduinos. The commands on the queue q are of the form (command, args), where command is an identifier indicating the type of instruction, and the args is a possibly empty tuple with the attributes to follow thru. <----SKIPPED LINES----> Returns: A 2-tuple of the (possibly-updated) message_queue and next_message_time. """ while not q.empty(): command, args = q.get() if command == 'pin': UpdateStatusLight(*args) elif command == 'replay': # a command might request info about flight to be (re)displayed, irrespective of # whether the screen is on; if so, let's put that message at the front of the message # queue, and delete any subsequent messages in queue because presumably the button # was pushed either a) when the screen was off (so no messages in queue), or b) # because the screen was on, but the last flight details got lost after other screens # that we're no longer interested in messageboard_flight_index = IdentifyFlightDisplayed( flights, configuration, display_all_hours=True) if messageboard_flight_index is not None: message_queue = DeleteMessageTypes(message_queue, (FLAG_MSG_INSIGHT, )) flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index]) message_queue.insert(0, (FLAG_MSG_FLIGHT, flight_message)) next_message_time = time.time() elif command == 'histogram': if not flights: Log('Histogram requested by remote %s but no flights in memory' % str(args)) else: histogram_type, histogram_history = args message_queue.extend(MessageboardHistograms( flights, histogram_type, histogram_history, '_1', False)) elif command == 'update_configuration': updated_settings = args[0] Log('Updated settings received from arduino: %s' % updated_settings) WriteFile(CONFIG_FILE, updated_settings) else: <----SKIPPED LINES----> Returns: Next_message_time, potentially updated if a message has been displayed, or unchanged if no message was displayed. """ if message_queue and (time.time() >= next_message_time or SIMULATION): if SIMULATION: # drain the queue because the messages come so fast messages_to_display = list(message_queue) # passed by reference, so clear it out since we drained it to the display del message_queue[:] else: # display only one message, being mindful of the display timing messages_to_display = [message_queue.pop(0)] for message in messages_to_display: message_text = message[1] if isinstance(message_text, str): message_text = textwrap.wrap(message_text, width=SPLITFLAP_CHARS_PER_LINE) display_message = Screenify(message_text, False) Log(display_message, file=ALL_MESSAGE_FILE) # This allows us to identify whats currently on the screen PickleObjectToFile(message, PICKLE_SCREENS, True) MaintainRollingWebLog(display_message, 25) if not SIMULATION: splitflap_message = Screenify(message_text, True) PublishMessage(splitflap_message) next_message_time = time.time() + configuration['setting_delay'] return next_message_time def DeleteMessageTypes(q, types_to_delete): """Delete messages from the queue if type is in the iterable types.""" if VERBOSE: messages_to_delete = [m for m in q if m[0] in types_to_delete] if messages_to_delete: Log('Deleting messages from queue due to new-found plane: %s' % messages_to_delete) updated_q = [m for m in q if m[0] not in types_to_delete] return updated_q def BootstrapInsightList(full_path=PICKLE_FLIGHTS): """(Re)populate flight pickle files with flight insight distributions. The set of insights generated for each flight is created at the time the flight was first identified, and saved on the flight pickle. This saving allows the current running distribution to be recalculated very quickly, but it means that as code enabling new insights gets added, those historical distributions may not necessarily be considered correct. They are "correct" in the sense that that new insight was not available at the time that older flight was seen, but it is not correct in the sense that, because this new insight is starting out with an incidence in the historical data of zero, this new insight may be reported more frequently than desired until it "catches up". So this method replays the flight history with the latest insight code, regenerating the insight distribution for each flight. """ directory, file = os.path.split(full_path) <----SKIPPED LINES----> os.kill(pid, signal.SIGTERM) init_timing.append((time.time(), 2)) SetPinMode() configuration = ReadAndParseSettings(CONFIG_FILE) Log('Read CONFIG_FILE at %s: %s' % (CONFIG_FILE, str(configuration))) startup_time = time.time() json_desc_dict = {} init_timing.append((time.time(), 3)) flights = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=MAX_INSIGHT_HORIZON_DAYS) # Clear the loaded flight of any cached data, identified by keys with a specific # suffix, since code fixes may change the values for some of those cached elements for flight in flights: for key in list(flight.keys()): if key.endswith(CACHED_ELEMENT_PREFIX): flight.pop(key) init_timing.append((time.time(), 4)) screen_history = UnpickleObjectFromFile(PICKLE_SCREENS, True, max_days=2) # If we're displaying just a single insight message, we want it to be something # unique, to the extent possible; this dict holds a count of the diff types of messages # displayed so far insight_message_distribution = {} # bootstrap the flight insights distribution from a list of insights on each # flight (i.e.: flight['insight_types'] for a given flight might look like # [1, 2, 7, 9], or [], to indicate which insights were identified; this then # transforms that into {0: 25, 1: 18, ...} summing across all flights. missing_insights = [] for flight in flights: if 'insight_types' not in flight: missing_insights.append( '%s on %s' % (DisplayFlightNumber(flight), DisplayTime(flight, '%x %X'))) distribution = flight.get('insight_types', []) for key in distribution: insight_message_distribution[key] = ( insight_message_distribution.get(key, 0) + 1) if missing_insights: <----SKIPPED LINES----> next_loop_time = time.time() + LOOP_DELAY_SECONDS # These files are read only if the version on disk has been modified more recently # than the last time it was read last_dump_json_timestamp = 0 init_timing.append((time.time(), 6)) WaitUntilKillComplete(already_running_ids) init_timing.append((time.time(), 7)) LogTimes(init_timing) Log('Finishing initialization of %d; starting radio polling loop' % os.getpid()) while (not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS)) and not SHUTDOWN_SIGNAL: last_heartbeat_time = Heartbeat(last_heartbeat_time) new_configuration = ReadAndParseSettings(CONFIG_FILE) UpdateRollingLogSize(new_configuration) CheckForNewFilterCriteria(configuration, new_configuration, message_queue, flights) configuration = new_configuration ResetLogs(configuration) # clear the logs if requested UpdateRollingLogSize(configuration) # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation, # then only read & do related processing for the next dump if the last-modified # timestamp indicates the file has been updated since it was last read. tmp_timestamp = 0 if not SIMULATION: dump_json_exists = os.path.exists(DUMP_JSON_FILE) if dump_json_exists: tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE) if (SIMULATION and DumpJsonChanges()) or ( not SIMULATION and dump_json_exists and tmp_timestamp > last_dump_json_timestamp): last_dump_json_timestamp = tmp_timestamp (persistent_nearby_aircraft, flight, now, json_desc_dict, persistent_path) = ScanForNewFlights( persistent_nearby_aircraft, persistent_path, configuration.get('log_jsons', False)) # because this might just be an updated instance of the previous flight as more # identifier information (squawk and or flight number) comes in, we only want to # process this if its a truly new flight new_flight_flag = ConfirmNewFlight(flight, flights) if new_flight_flag: flights.append(flight) remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration, screen_history) flight_meets_display_criteria = FlightMeetsDisplayCriteria( flight, configuration, log=True) if flight_meets_display_criteria: flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight), flight) # display the next message about this flight now! next_message_time = time.time() message_queue.insert(0, flight_message) # and delete any queued insight messages about other flights that have # not yet displayed, since a newer flight has taken precedence message_queue = DeleteMessageTypes(message_queue, (FLAG_MSG_INSIGHT,)) # Though we also manage the message queue outside this conditional as well, # because it can take a half second to generate the flight insights, this allows # this message to start displaying on the board immediately, so it's up there # when it's most relevant next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration) insight_messages = CreateFlightInsights( flights, configuration.get('insights'), insight_message_distribution) if configuration.get('next_flight', 'off') == 'on': next_flight_text = FlightInsightNextFlight(flights, configuration) if next_flight_text: insight_messages.insert(0, next_flight_text) insight_messages = [(FLAG_MSG_INSIGHT, m) for m in insight_messages] for insight_message in insight_messages: message_queue.insert(0, insight_message) else: # flight didn't meet display criteria flight['insight_types'] = [] PickleObjectToFile(flight, PICKLE_FLIGHTS, True, timestamp=flight['now']) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration, screen_history) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) PersonalMessage(configuration, message_queue) if SIMULATION: if now: simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z') if simulated_hour != prev_simulated_hour: print(simulated_hour) prev_simulated_hour = simulated_hour histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE) RemoveFile(HISTOGRAM_CONFIG_FILE) # We also need to make sure there are flights on which to generate a histogram! Why # might there not be any flights? Primarily during a simulation, if there's a # lingering histogram file at the time of history restart. if histogram and not flights: <----SKIPPED LINES----> |