01234567890123456789012345678901234567890123456789012345678901234567890123456789
11061107110811091110111111121113111411151116111711181119112011211122112311241125 112611271128112911301131113211331134 11351136 113711381139 1140114111421143114411451146 11471148114911501151115211531154115511561157115811591160116111621163116411651166 123412351236123712381239124012411242124312441245124612471248124912501251125212531254 125512561257125812591260126112621263126412651266126712681269127012711272127312741275 13891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429 4690469146924693469446954696469746984699470047014702470347044705470647074708470947104711 47124713471447154716471747184719472047214722472347244725472647274728472947304731 49844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024 51335134513551365137513851395140514151425143514451455146514751485149515051515152 515351545155 5156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186 524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334 53355336533753385339534053415342534353445345534653475348534953505351535253535354 53975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440 54545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479 54805481548254835484548554865487548854895490549154925493549454955496549754985499 56195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662 | <----SKIPPED LINES----> files = [full_path] else: return [] data = [] if filenames: return files for file in files: try: with open(file, 'rb') as f: while True: data.append(pickle.load(f)) except (EOFError, pickle.UnpicklingError): pass return data def PickleObjectToFile(data, full_path, date_segmentation, date_suffix=None): """Append one pickled flight to the end of binary file. Args: data: data to pickle full_path: name (potentially including path) of the pickled file date_segmentation: boolean indicating whether the date string yyyy-mm-dd should be prepended to the file name in full_path based on the current date, so that pickled files are segmented by date. """ if not date_suffix: date_suffix = EpochDisplayTime(time.time(), '%Y-%m-%d-') if date_segmentation: full_path = PrependFileName(full_path, date_suffix) try: with open(full_path, 'ab') as f: f.write(pickle.dumps(data)) except IOError: Log('Unable to append pickle ' + full_path) def UpdateAircraftList(persistent_nearby_aircraft, current_nearby_aircraft, now): """Identifies newly seen aircraft and removes aircraft that haven't been seen recently. Updates persistent_nearby_aircraft as follows: flights that have been last seen more than PERSISTENCE_SECONDS seconds ago are removed; new flights in current_nearby_aircraft are added. Also identifies newly-seen aircraft and updates the last-seen timestamp of flights that have been seen again. Args: persistent_nearby_aircraft: dictionary where keys are flight number / squawk tuples, and the values are the time the flight was last seen. current_nearby_aircraft: dictionary where keys are flight numbers / squawk tuples, and the values are themselves dictionaries with key-value pairs about that flight, with at least one of the kv-pairs being the time the flight was seen. now: the timestamp of the flights in the current_nearby_aircraft. Returns: A list of newly-nearby flight identifiers (i.e.: 2-tuple of flight number / squawk). <----SKIPPED LINES----> if len(newly_nearby_flight_identifiers) > 1: newly_nearby_flight_identifiers_str = ', '.join(newly_nearby_flight_identifiers) newly_nearby_flight_details_str = '\n'.join( [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers]) Log('Multiple newly-nearby flights: %s\n%s' % ( newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str)) flight_identifier = newly_nearby_flight_identifiers[0] flight_aware_json = {} if SIMULATION: json_times = [j[1] for j in FA_JSONS] if json_time in json_times: flight_aware_json = FA_JSONS[json_times.index(json_time)][0] elif flight_identifier[0]: flight_number = flight_identifier[0] flight_aware_json = GetFlightAwareJson(flight_number) if flight_aware_json: UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False) else: Log('No json returned from Flightaware for flight: %s' % flight_number) UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True) flight_details = {} if flight_aware_json: flight_details = ParseFlightAwareJson(flight_aware_json) if not SIMULATION and log_jsons: PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True) # Augment FlightAware details with radio / radio-derived details flight_details.update(current_nearby_aircraft[flight_identifier]) # Augment with the past location data; the [1] is because recall that # persistent_path[key] is actually a 2-tuple, the first element being # the most recent time seen, and the second element being the actual # path. But we do not need to keep around the most recent time seen any # more. flight_details['persistent_path'] = persistent_path[flight_identifier][1] return ( persistent_nearby_aircraft, <----SKIPPED LINES----> Args: dump_json: The text representation of the json message from dump1090-mutability persistent_path: dictionary where keys are flight numbers, and the values are a sequential list of the location-attributes in the json file; allows for tracking the flight path over time. Returns: Return tuple: - dictionary of all nearby planes, where keys are flight numbers (i.e.: 'SWA7543'), and the value is itself a dictionary of attributes. - time stamp in the json file. - dictionary of attributes about the radio range - persistent dictionary of the track of recent flights, where keys are the flight numbers and the value is a tuple, the first element being when the flight was last seen in this radio, and the second is a list of dictionaries with past location info from the radio where it's been seen, i.e.: d[flight] = (timestamp, [{}, {}, {}]) """ parsed = json.loads(dump_json) now = parsed['now'] print(EpochDisplayTime(now)) #TODO nearby_aircraft = {} # Build dictionary summarizing characteristics of the dump_json itself json_desc_dict = DescribeDumpJson(parsed) for aircraft in parsed['aircraft']: simplified_aircraft = {} simplified_aircraft['now'] = now # flight_number flight_number = aircraft.get('flight') if flight_number: flight_number = flight_number.strip() # squawk squawk = aircraft.get('squawk') if squawk: squawk = squawk.strip() <----SKIPPED LINES----> FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True) global ALL_MESSAGE_FILE ALL_MESSAGE_FILE = PrependFileName(ALL_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ALL_MESSAGE_FILE) global LOGFILE LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX) ClearFile(LOGFILE) global ROLLING_LOGFILE ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX) ClearFile(ROLLING_LOGFILE) global ROLLING_MESSAGE_FILE ROLLING_MESSAGE_FILE = PrependFileName(ROLLING_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ROLLING_MESSAGE_FILE) global PICKLE_FLIGHTS PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX) ClearFile(PICKLE_FLIGHTS) filenames = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=None, filenames=True) for file in filenames: ClearFile(file) def SimulationEnd(message_queue, flights): """Clears message buffer, exercises histograms, and other misc test & status code. Args: message_queue: List of flight messages that have not yet been printed. flights: List of flights dictionaries. """ if flights: histogram = { 'type': 'both', 'histogram':'all', 'histogram_history':'30d', 'histogram_max_screens': '_2', 'histogram_data_summary': 'on'} message_queue.extend(TriggerHistograms(flights, histogram)) <----SKIPPED LINES----> args=(to_remote_q, to_main_q, shutdown[0])) servo = ValidateSingleRunning( 'enable_servos' in configuration, arduino.ServoMain, p=servo, args=(to_servo_q, to_main_q, shutdown[1])) return remote, servo def ValidateSingleRunning(enabled, start_function, p=None, args=()): """Restarts a new instance of multiprocessing process if not running""" if not SHUTDOWN_SIGNAL: if not enabled: if p is not None: # must have just requested a disabling of single instance args[2].value = 1 # trigger a shutdown on the single instance return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) else: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = True # has been set to True #TODO p.start() return p def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q): """Send latest data to arduinos via their shared-memory queues""" last_flight = {} if flights: last_flight = flights[-1] if SIMULATION: now = json_desc_dict['now'] else: now = time.time() <----SKIPPED LINES----> # See https://stackoverflow.com/questions/31826814/curl-post-request-into-pycurl-code # Set URL value curl.setopt( pycurl.URL, 'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id) curl.setopt(pycurl.HTTPHEADER, [ 'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret]) curl.setopt(pycurl.TIMEOUT_MS, timeout*1000) curl.setopt(pycurl.POST, 1) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean # preparing body the way pycurl.READDATA wants it body_as_dict = {'text': s} body_as_json_string = json.dumps(body_as_dict) # dict to json body_as_file_object = io.StringIO(body_as_json_string) # prepare and send. See also: pycurl.READFUNCTION to pass function instead curl.setopt(pycurl.READDATA, body_as_file_object) curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string)) try: curl.perform() except pycurl.error as e: Log('curl.perform() failed with message %s' % e) error_code = True else: # you may want to check HTTP response code, e.g. status_code = curl.getinfo(pycurl.RESPONSE_CODE) if status_code != 200: Log('Server returned HTTP status code %d for message %s' % (status_code, s)) error_code = True curl.close() UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, error_code) def ManageMessageQueue(message_queue, next_message_time, configuration): """Check time & if appropriate, display next message from queue. Args: message_queue: FIFO list of message tuples of (message type, message string). next_message_time: epoch at which next message should be displayed configuration: dictionary of configuration attributes. Returns: Next_message_time, potentially updated if a message has been displayed, or unchanged if no message was displayed. """ if message_queue and (time.time() >= next_message_time or SIMULATION): if SIMULATION: # drain the queue because the messages come so fast messages_to_display = list(message_queue) # passed by reference, so clear it out since we drained it to the display del message_queue[:] <----SKIPPED LINES----> def ResetLogs(config): """Clears the non-scrolling logs if reset_logs in config.""" if 'reset_logs' in config: Log('Reset logs') for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE): if RemoveFile(f): open(f, 'a').close() config.pop('reset_logs') config = BuildSettings(config) WriteFile(CONFIG_FILE, config) return config def CheckTemperature(): """Turn on fan if temperature exceeds threshold.""" if RASPBERRY_PI: temperature = gpiozero.CPUTemperature().temperature if temperature > TEMP_FAN_TURN_ON_CELSIUS: UpdateStatusLight(GPIO_FAN, True) elif temperature < TEMP_FAN_TURN_OFF_CELSIUS: UpdateStatusLight(GPIO_FAN, False) pin_values = {} # caches last set value def SetPinMode(): """Initialize output GPIO pins for output on Raspberry Pi.""" global pin_values if RASPBERRY_PI: RPi.GPIO.setmode(RPi.GPIO.BCM) pins = ( GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION, GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2) for pin in pins: initial_state = pin[5] pin_values[pin[0]] = initial_state # Initialize state of pins UpdateDashboard(initial_state, pin) if RASPBERRY_PI: RPi.GPIO.setup(pin[0], RPi.GPIO.OUT) RPi.GPIO.output(pin[0], pin_values[pin[0]]) UpdateDashboard(pin_values[pin[0]], pin) if RASPBERRY_PI: # configure soft reset button RPi.GPIO.setup(GPIO_SOFT_RESET[0], RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN) RPi.GPIO.setup(GPIO_SOFT_RESET[1], RPi.GPIO.OUT) RPi.GPIO.output(GPIO_SOFT_RESET[1], True) RPi.GPIO.add_event_detect(GPIO_SOFT_RESET[0], RPi.GPIO.RISING) RPi.GPIO.add_event_callback(GPIO_SOFT_RESET[0], InterruptRebootFromButton) def UpdateStatusLight(pin, value): """Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value.""" global pin_values if value: msg = pin[1] else: msg = pin[2] if RASPBERRY_PI: RPi.GPIO.output(pin[0], value) if value: pin_setting = 'HIGH' relay_light_value = 'OFF' else: pin_setting = 'LOW' relay_light_value = 'ON' msg += '; RPi GPIO pin %d set to %s; relay light #%d should now be %s' % ( pin[0], pin_setting, pin[3], relay_light_value) if pin_values[pin[0]] != value: if VERBOSE: Log(msg) # log pin_values[pin[0]] = value # update cache UpdateDashboard(value, pin) def UpdateDashboard(value, subsystem=0): versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO) if subsystem: subsystem = subsystem[0] PickleObjectToFile((time.time(), subsystem, value, versions), PICKLE_DASHBOARD, True) def RemoveFile(file): """Removes a file if it exists, returning a boolean indicating if it had existed.""" if os.path.exists(file): os.remove(file) return True return False def ConfirmNewFlight(flight, flights): """Replaces last-seen flight with new flight if otherwise identical but for identifiers. Flights are identified by the radio over time by a tuple of identifiers: flight_number and squawk. Due to unknown communication issues, one or the other may not always be transmitted. However, as soon as a new flight is identified that has at least one of those identifiers, we report on it and log it to the pickle repository, etc. This function checks if the newly identified flight is indeed a duplicate of the immediate prior flight by virtue of having the same squawk and/or flight number, and <----SKIPPED LINES----> # There is potential complication in that the last flight and the new flight # crossed into a new day, and we are using date segmentation so that the last # flight exists in yesterday's file max_days = 1 if not SIMULATION and DisplayTime(flight, '%x') != DisplayTime(last_flight, '%x'): max_days = 2 message += ( '; in repickling, we crossed days, so pickled flights that might otherwise' ' be in %s file are now all located in %s file' % ( DisplayTime(last_flight, '%x'), DisplayTime(flight, '%x'))) Log(message) args = (PICKLE_FLIGHTS, not SIMULATION, max_days) saved_flights = UnpickleObjectFromFile(*args)[:-1] files_to_overwrite = UnpickleObjectFromFile(*args, filenames=True) for file in files_to_overwrite: os.remove(file) for f in saved_flights: if SPLIT_SIMULATION_FLIGHT_PICKLE: PickleObjectToFile(f, PICKLE_FLIGHTS, True, date_suffix=DisplayTime(f, '%Y-%m-%d-')) else: PickleObjectToFile(f, PICKLE_FLIGHTS, not SIMULATION) return False def HeartbeatRestart(): if SIMULATION: return 0 UpdateDashboard(True) # Indicates that this wasn't running a moment before, ... UpdateDashboard(False) # ... and now it is running! return time.time() def Heartbeat(last_heartbeat_time): if SIMULATION: return last_heartbeat_time now = time.time() if now - last_heartbeat_time > HEARTBEAT_SECONDS: UpdateDashboard(False) last_heartbeat_time = now return last_heartbeat_time <----SKIPPED LINES----> last_modified_suffix = EpochDisplayTime(epoch, format_string='-%Y-%m-%d-%H%M') version_name = python_prefix + last_modified_suffix + file_extension version_path = os.path.join(VERSION_REPOSITORY, version_name) shutil.copyfile(live_path, version_path) return version_name VERSION_MESSAGEBOARD = MakeCopy('messageboard') VERSION_ARDUINO = MakeCopy('arduino') def main(): """Traffic cop between incoming radio flight messages, configuration, and messageboard. This is the main logic, checking for new flights, augmenting the radio signal with additional web-scraped data, and generating messages in a form presentable to the messageboard. """ RemoveFile(LOGFILE_LOCK) VersionControl() last_heartbeat_time = HeartbeatRestart() # Since this clears log files, it should occur first before we start logging if '-s' in sys.argv: global SIMULATION_COUNTER SimulationSetup() # This flag slows down simulation time around a flight, great for debugging the arduinos simulation_slowdown = bool('-f' in sys.argv) # Redirect any errors to a log file instead of the screen, and add a datestamp if not SIMULATION: sys.stderr = open(STDERR_FILE, 'a') Log('', STDERR_FILE) Log('Starting up process %d' % os.getpid()) already_running_ids = FindRunningParents() if already_running_ids: for pid in already_running_ids: Log('Sending termination signal to %d' % pid) os.kill(pid, signal.SIGTERM) SetPinMode() configuration = ReadAndParseSettings(CONFIG_FILE) startup_time = time.time() <----SKIPPED LINES----> # this message to start displaying on the board immediately, so it's up there # when it's most relevant next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration) insight_messages = CreateFlightInsights( flights, configuration.get('insights'), insight_message_distribution) if configuration.get('next_flight', 'off') == 'on': next_flight_text = FlightInsightNextFlight(flights, configuration) if next_flight_text: insight_messages.insert(0, next_flight_text) insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages] for insight_message in insight_messages: message_queue.insert(0, insight_message) else: # flight didn't meet display criteria flight['insight_types'] = [] if SPLIT_SIMULATION_FLIGHT_PICKLE: PickleObjectToFile(flight, PICKLE_FLIGHTS, True, date_suffix=DisplayTime(flight, '%Y-%m-%d-')) else: PickleObjectToFile(flight, PICKLE_FLIGHTS, not SIMULATION) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) if SIMULATION: if now: simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z') if simulated_hour != prev_simulated_hour: print(simulated_hour) prev_simulated_hour = simulated_hour histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE) RemoveFile(HISTOGRAM_CONFIG_FILE) <----SKIPPED LINES----> |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
1106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211 1279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321 14351436143714381439144014411442144314441445144614471448144914501451145214531454 14551456145714581459146014611462146314641465146614671468146914701471147214731474 47354736473747384739474047414742474347444745474647474748474947504751475247534754 475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781 50345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074 51835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238 5300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408 54515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494 55085509551055115512551355145515551655175518551955205521552255235524552555265527 552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554 56745675567656775678567956805681568256835684568556865687568856895690569156925693 5694 56955696569756985699570057015702570357045705570657075708570957105711571257135714 | <----SKIPPED LINES----> files = [full_path] else: return [] data = [] if filenames: return files for file in files: try: with open(file, 'rb') as f: while True: data.append(pickle.load(f)) except (EOFError, pickle.UnpicklingError): pass return data cached_object_count = {} def PickleObjectToFile(data, full_path, date_segmentation, timestamp=None, verify=False): """Append one pickled flight to the end of binary file. Args: data: data to pickle full_path: name (potentially including path) of the pickled file date_segmentation: boolean indicating whether the date string yyyy-mm-dd should be prepended to the file name in full_path based on the current date, so that pickled files are segmented by date. timestamp: if date_segmentation is True, this is used rather than system time to generate the file name. verify: boolean indicating if we should verify that the pickled file object count increments by one, rewriting entire pickle file if it doesn't. Note that since this requires reading the entire pickle file and unpickling, it should only be done for small files / objects. Returns: Name of file to which the data was pickled if successful; None if failed. """ global cached_object_count if not timestamp: timestamp = time.time() date_suffix = EpochDisplayTime(timestamp, '%Y-%m-%d-') if date_segmentation: full_path = PrependFileName(full_path, date_suffix) if full_path not in cached_object_count: cached_object_count[full_path] = len(UnpickleObjectFromFile(full_path, False)) if not os.path.exists(full_path): # Another method may delete the file cached_object_count[full_path] = 0 try: with open(full_path, 'ab') as f: f.write(pickle.dumps(data)) except IOError: Log('Unable to append pickle ' + full_path) return None if verify: # file object count should now be one more; if it isn't, the file is corrupted, and # rather than continue writing to a corrupted pickle file, we should fix it so we # don't lose too much data pickled_data = UnpickleObjectFromFile(full_path, False) cached_count = cached_object_count[full_path] if len(pickled_data) == cached_count + 1: cached_object_count[full_path] = cached_count + 1 else: tmp_file_name = full_path + '.tmp' try: with open(tmp_file_name, 'ab') as f: for d in pickled_data: # rewrite the old data that was retained f.write(pickle.dumps(d)) f.write(pickle.dumps(data)) # new data except IOError: Log('Unable to append pickle %s in verify step; left tmp file as-is' % tmp_file_name) return None shutil.move(tmp_file_name, full_path) cached_object_count[full_path] = len(pickled_data) + 1 Log('Re-pickled %s: after writing %s, expected len %d to increment, ' 'but it did not; after repickling (and adding the new data), new length = %d' % ( full_path, data, cached_count, cached_object_count[full_path])) return full_path def UpdateAircraftList(persistent_nearby_aircraft, current_nearby_aircraft, now): """Identifies newly seen aircraft and removes aircraft that haven't been seen recently. Updates persistent_nearby_aircraft as follows: flights that have been last seen more than PERSISTENCE_SECONDS seconds ago are removed; new flights in current_nearby_aircraft are added. Also identifies newly-seen aircraft and updates the last-seen timestamp of flights that have been seen again. Args: persistent_nearby_aircraft: dictionary where keys are flight number / squawk tuples, and the values are the time the flight was last seen. current_nearby_aircraft: dictionary where keys are flight numbers / squawk tuples, and the values are themselves dictionaries with key-value pairs about that flight, with at least one of the kv-pairs being the time the flight was seen. now: the timestamp of the flights in the current_nearby_aircraft. Returns: A list of newly-nearby flight identifiers (i.e.: 2-tuple of flight number / squawk). <----SKIPPED LINES----> if len(newly_nearby_flight_identifiers) > 1: newly_nearby_flight_identifiers_str = ', '.join(newly_nearby_flight_identifiers) newly_nearby_flight_details_str = '\n'.join( [str(current_nearby_aircraft[f]) for f in newly_nearby_flight_identifiers]) Log('Multiple newly-nearby flights: %s\n%s' % ( newly_nearby_flight_identifiers_str, newly_nearby_flight_details_str)) flight_identifier = newly_nearby_flight_identifiers[0] flight_aware_json = {} if SIMULATION: json_times = [j[1] for j in FA_JSONS] if json_time in json_times: flight_aware_json = FA_JSONS[json_times.index(json_time)][0] elif flight_identifier[0]: flight_number = flight_identifier[0] flight_aware_json = GetFlightAwareJson(flight_number) if flight_aware_json: UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, False) else: failure_message = 'No json from Flightaware for flight: %s' % flight_number Log(failure_message) UpdateStatusLight(GPIO_ERROR_FLIGHT_AWARE_CONNECTION, True, failure_message) flight_details = {} if flight_aware_json: flight_details = ParseFlightAwareJson(flight_aware_json) if not SIMULATION and log_jsons: PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE, True) # Augment FlightAware details with radio / radio-derived details flight_details.update(current_nearby_aircraft[flight_identifier]) # Augment with the past location data; the [1] is because recall that # persistent_path[key] is actually a 2-tuple, the first element being # the most recent time seen, and the second element being the actual # path. But we do not need to keep around the most recent time seen any # more. flight_details['persistent_path'] = persistent_path[flight_identifier][1] return ( persistent_nearby_aircraft, <----SKIPPED LINES----> Args: dump_json: The text representation of the json message from dump1090-mutability persistent_path: dictionary where keys are flight numbers, and the values are a sequential list of the location-attributes in the json file; allows for tracking the flight path over time. Returns: Return tuple: - dictionary of all nearby planes, where keys are flight numbers (i.e.: 'SWA7543'), and the value is itself a dictionary of attributes. - time stamp in the json file. - dictionary of attributes about the radio range - persistent dictionary of the track of recent flights, where keys are the flight numbers and the value is a tuple, the first element being when the flight was last seen in this radio, and the second is a list of dictionaries with past location info from the radio where it's been seen, i.e.: d[flight] = (timestamp, [{}, {}, {}]) """ parsed = json.loads(dump_json) now = parsed['now'] nearby_aircraft = {} # Build dictionary summarizing characteristics of the dump_json itself json_desc_dict = DescribeDumpJson(parsed) for aircraft in parsed['aircraft']: simplified_aircraft = {} simplified_aircraft['now'] = now # flight_number flight_number = aircraft.get('flight') if flight_number: flight_number = flight_number.strip() # squawk squawk = aircraft.get('squawk') if squawk: squawk = squawk.strip() <----SKIPPED LINES----> FA_JSONS = UnpickleObjectFromFile(PICKLE_FA_JSON_FILE, True) global ALL_MESSAGE_FILE ALL_MESSAGE_FILE = PrependFileName(ALL_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ALL_MESSAGE_FILE) global LOGFILE LOGFILE = PrependFileName(LOGFILE, SIMULATION_PREFIX) ClearFile(LOGFILE) global ROLLING_LOGFILE ROLLING_LOGFILE = PrependFileName(ROLLING_LOGFILE, SIMULATION_PREFIX) ClearFile(ROLLING_LOGFILE) global ROLLING_MESSAGE_FILE ROLLING_MESSAGE_FILE = PrependFileName(ROLLING_MESSAGE_FILE, SIMULATION_PREFIX) ClearFile(ROLLING_MESSAGE_FILE) global PICKLE_FLIGHTS PICKLE_FLIGHTS = PrependFileName(PICKLE_FLIGHTS, SIMULATION_PREFIX) filenames = UnpickleObjectFromFile(PICKLE_FLIGHTS, True, max_days=None, filenames=True) for file in filenames: ClearFile(file) global PICKLE_DASHBOARD PICKLE_DASHBOARD = PrependFileName(PICKLE_DASHBOARD, SIMULATION_PREFIX) filenames = UnpickleObjectFromFile(PICKLE_DASHBOARD, True, max_days=None, filenames=True) for file in filenames: ClearFile(file) def SimulationEnd(message_queue, flights): """Clears message buffer, exercises histograms, and other misc test & status code. Args: message_queue: List of flight messages that have not yet been printed. flights: List of flights dictionaries. """ if flights: histogram = { 'type': 'both', 'histogram':'all', 'histogram_history':'30d', 'histogram_max_screens': '_2', 'histogram_data_summary': 'on'} message_queue.extend(TriggerHistograms(flights, histogram)) <----SKIPPED LINES----> args=(to_remote_q, to_main_q, shutdown[0])) servo = ValidateSingleRunning( 'enable_servos' in configuration, arduino.ServoMain, p=servo, args=(to_servo_q, to_main_q, shutdown[1])) return remote, servo def ValidateSingleRunning(enabled, start_function, p=None, args=()): """Restarts a new instance of multiprocessing process if not running""" if not SHUTDOWN_SIGNAL: if not enabled: if p is not None: # must have just requested a disabling of single instance args[2].value = 1 # trigger a shutdown on the single instance return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) elif VERBOSE: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = True # has been set to True #TODO p.start() return p def EnqueueArduinos(flights, json_desc_dict, configuration, to_servo_q, to_remote_q): """Send latest data to arduinos via their shared-memory queues""" last_flight = {} if flights: last_flight = flights[-1] if SIMULATION: now = json_desc_dict['now'] else: now = time.time() <----SKIPPED LINES----> # See https://stackoverflow.com/questions/31826814/curl-post-request-into-pycurl-code # Set URL value curl.setopt( pycurl.URL, 'https://platform.vestaboard.com/subscriptions/%s/message' % subscription_id) curl.setopt(pycurl.HTTPHEADER, [ 'X-Vestaboard-Api-Key:%s' % key, 'X-Vestaboard-Api-Secret:%s' % secret]) curl.setopt(pycurl.TIMEOUT_MS, timeout*1000) curl.setopt(pycurl.POST, 1) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) # to keep stdout clean # preparing body the way pycurl.READDATA wants it body_as_dict = {'text': s} body_as_json_string = json.dumps(body_as_dict) # dict to json body_as_file_object = io.StringIO(body_as_json_string) # prepare and send. See also: pycurl.READFUNCTION to pass function instead curl.setopt(pycurl.READDATA, body_as_file_object) curl.setopt(pycurl.POSTFIELDSIZE, len(body_as_json_string)) failure_message = '' try: curl.perform() except pycurl.error as e: failure_message = 'curl.perform() failed with message %s' % e Log('curl.perform() failed with message %s' % e) error_code = True else: # you may want to check HTTP response code, e.g. status_code = curl.getinfo(pycurl.RESPONSE_CODE) if status_code != 200: Log('Server returned HTTP status code %d for message %s' % (status_code, s)) error_code = True curl.close() UpdateStatusLight(GPIO_ERROR_VESTABOARD_CONNECTION, error_code, failure_message) def ManageMessageQueue(message_queue, next_message_time, configuration): """Check time & if appropriate, display next message from queue. Args: message_queue: FIFO list of message tuples of (message type, message string). next_message_time: epoch at which next message should be displayed configuration: dictionary of configuration attributes. Returns: Next_message_time, potentially updated if a message has been displayed, or unchanged if no message was displayed. """ if message_queue and (time.time() >= next_message_time or SIMULATION): if SIMULATION: # drain the queue because the messages come so fast messages_to_display = list(message_queue) # passed by reference, so clear it out since we drained it to the display del message_queue[:] <----SKIPPED LINES----> def ResetLogs(config): """Clears the non-scrolling logs if reset_logs in config.""" if 'reset_logs' in config: Log('Reset logs') for f in (STDERR_FILE, BACKUP_FILE, SERVICE_VERIFICATION_FILE): if RemoveFile(f): open(f, 'a').close() config.pop('reset_logs') config = BuildSettings(config) WriteFile(CONFIG_FILE, config) return config def CheckTemperature(): """Turn on fan if temperature exceeds threshold.""" if RASPBERRY_PI: temperature = gpiozero.CPUTemperature().temperature if temperature > TEMP_FAN_TURN_ON_CELSIUS: UpdateStatusLight(GPIO_FAN, True, 'Temperature: %.1f' % temperature) elif temperature < TEMP_FAN_TURN_OFF_CELSIUS: UpdateStatusLight(GPIO_FAN, False) pin_values = {} # caches last set value def SetPinMode(): """Initialize output GPIO pins for output on Raspberry Pi.""" global pin_values if RASPBERRY_PI: RPi.GPIO.setmode(RPi.GPIO.BCM) pins = ( GPIO_ERROR_VESTABOARD_CONNECTION, GPIO_ERROR_FLIGHT_AWARE_CONNECTION, GPIO_ERROR_ARDUINO_SERVO_CONNECTION, GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, GPIO_ERROR_BATTERY_CHARGE, GPIO_FAN, GPIO_UNUSED_1, GPIO_UNUSED_2) for pin in pins: initial_state = pin[5] pin_values[pin[0]] = initial_state # Initialize state of pins UpdateDashboard(initial_state, pin) if RASPBERRY_PI: RPi.GPIO.setup(pin[0], RPi.GPIO.OUT) RPi.GPIO.output(pin[0], pin_values[pin[0]]) UpdateDashboard(pin_values[pin[0]], pin) if RASPBERRY_PI: # configure soft reset button RPi.GPIO.setup(GPIO_SOFT_RESET[0], RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN) RPi.GPIO.setup(GPIO_SOFT_RESET[1], RPi.GPIO.OUT) RPi.GPIO.output(GPIO_SOFT_RESET[1], True) RPi.GPIO.add_event_detect(GPIO_SOFT_RESET[0], RPi.GPIO.RISING) RPi.GPIO.add_event_callback(GPIO_SOFT_RESET[0], InterruptRebootFromButton) def UpdateStatusLight(pin, value, failure_message=''): """Sets the Raspberry Pi GPIO pin high (True) or low (False) based on value.""" global pin_values if value: msg = pin[1] else: msg = pin[2] if RASPBERRY_PI: RPi.GPIO.output(pin[0], value) if value: pin_setting = 'HIGH' relay_light_value = 'OFF' else: pin_setting = 'LOW' relay_light_value = 'ON' msg += '; RPi GPIO pin %d set to %s; relay light #%d should now be %s' % ( pin[0], pin_setting, pin[3], relay_light_value) if pin_values[pin[0]] != value: if VERBOSE: Log(msg) # log pin_values[pin[0]] = value # update cache UpdateDashboard(value, subsystem=pin, failure_message=failure_message) def UpdateDashboard(value, subsystem=0, failure_message=''): versions = (VERSION_MESSAGEBOARD, VERSION_ARDUINO) if subsystem: subsystem = subsystem[0] PickleObjectToFile( (time.time(), subsystem, value, versions, failure_message), PICKLE_DASHBOARD, True) def RemoveFile(file): """Removes a file if it exists, returning a boolean indicating if it had existed.""" if os.path.exists(file): os.remove(file) return True return False def ConfirmNewFlight(flight, flights): """Replaces last-seen flight with new flight if otherwise identical but for identifiers. Flights are identified by the radio over time by a tuple of identifiers: flight_number and squawk. Due to unknown communication issues, one or the other may not always be transmitted. However, as soon as a new flight is identified that has at least one of those identifiers, we report on it and log it to the pickle repository, etc. This function checks if the newly identified flight is indeed a duplicate of the immediate prior flight by virtue of having the same squawk and/or flight number, and <----SKIPPED LINES----> # There is potential complication in that the last flight and the new flight # crossed into a new day, and we are using date segmentation so that the last # flight exists in yesterday's file max_days = 1 if not SIMULATION and DisplayTime(flight, '%x') != DisplayTime(last_flight, '%x'): max_days = 2 message += ( '; in repickling, we crossed days, so pickled flights that might otherwise' ' be in %s file are now all located in %s file' % ( DisplayTime(last_flight, '%x'), DisplayTime(flight, '%x'))) Log(message) args = (PICKLE_FLIGHTS, not SIMULATION, max_days) saved_flights = UnpickleObjectFromFile(*args)[:-1] files_to_overwrite = UnpickleObjectFromFile(*args, filenames=True) for file in files_to_overwrite: os.remove(file) for f in saved_flights: # we would like to use verify=True, but that's too slow without further optimizing the # verification step for a loop of data PickleObjectToFile( f, PICKLE_FLIGHTS, True, timestamp=f['now'], verify=False) return False def HeartbeatRestart(): if SIMULATION: return 0 UpdateDashboard(True) # Indicates that this wasn't running a moment before, ... UpdateDashboard(False) # ... and now it is running! return time.time() def Heartbeat(last_heartbeat_time): if SIMULATION: return last_heartbeat_time now = time.time() if now - last_heartbeat_time > HEARTBEAT_SECONDS: UpdateDashboard(False) last_heartbeat_time = now return last_heartbeat_time <----SKIPPED LINES----> last_modified_suffix = EpochDisplayTime(epoch, format_string='-%Y-%m-%d-%H%M') version_name = python_prefix + last_modified_suffix + file_extension version_path = os.path.join(VERSION_REPOSITORY, version_name) shutil.copyfile(live_path, version_path) return version_name VERSION_MESSAGEBOARD = MakeCopy('messageboard') VERSION_ARDUINO = MakeCopy('arduino') def main(): """Traffic cop between incoming radio flight messages, configuration, and messageboard. This is the main logic, checking for new flights, augmenting the radio signal with additional web-scraped data, and generating messages in a form presentable to the messageboard. """ RemoveFile(LOGFILE_LOCK) VersionControl() # Since this clears log files, it should occur first before we start logging if '-s' in sys.argv: global SIMULATION_COUNTER SimulationSetup() last_heartbeat_time = HeartbeatRestart() # This flag slows down simulation time around a flight, great for debugging the arduinos simulation_slowdown = bool('-f' in sys.argv) # Redirect any errors to a log file instead of the screen, and add a datestamp if not SIMULATION: sys.stderr = open(STDERR_FILE, 'a') Log('', STDERR_FILE) Log('Starting up process %d' % os.getpid()) already_running_ids = FindRunningParents() if already_running_ids: for pid in already_running_ids: Log('Sending termination signal to %d' % pid) os.kill(pid, signal.SIGTERM) SetPinMode() configuration = ReadAndParseSettings(CONFIG_FILE) startup_time = time.time() <----SKIPPED LINES----> # this message to start displaying on the board immediately, so it's up there # when it's most relevant next_message_time = ManageMessageQueue( message_queue, next_message_time, configuration) insight_messages = CreateFlightInsights( flights, configuration.get('insights'), insight_message_distribution) if configuration.get('next_flight', 'off') == 'on': next_flight_text = FlightInsightNextFlight(flights, configuration) if next_flight_text: insight_messages.insert(0, next_flight_text) insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages] for insight_message in insight_messages: message_queue.insert(0, insight_message) else: # flight didn't meet display criteria flight['insight_types'] = [] PickleObjectToFile(flight, PICKLE_FLIGHTS, True, timestamp=flight['now']) else: remote, servo = RefreshArduinos( remote, servo, to_remote_q, to_servo_q, to_main_q, shutdown, flights, json_desc_dict, configuration) message_queue, next_message_time = ProcessArduinoCommmands( to_main_q, flights, configuration, message_queue, next_message_time) if SIMULATION: if now: simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z') if simulated_hour != prev_simulated_hour: print(simulated_hour) prev_simulated_hour = simulated_hour histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE) RemoveFile(HISTOGRAM_CONFIG_FILE) <----SKIPPED LINES----> |