01234567890123456789012345678901234567890123456789012345678901234567890123456789
1234567891011121314151617 181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861 862863864865866 867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942 95695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004 10051006100710081009101010111012101310141015101610171018101910201021102210231024 107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094 109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181 118211831184118511861187118811891190119111921193119411951196119711981199120012011202 | #!/usr/bin/python3 import numbers import os import random import struct import subprocess import sys import termios import time import psutil import serial import serial.tools.list_ports from pySerialTransfer import pySerialTransfer import messageboard ARDUINO_LOG = 'arduino_log.txt' ARDUINO_ROLLING_LOG = 'arduino_rolling_log.txt' SERIALS_LOG = 'arduino_serials_log.txt' VERBOSE = False # log additional fine-grained details into ARDUINO_LOG LOG_SERIALS = False # log serial data sent from Arduino to ARDUINO_LOG SIMULATE_ARDUINO = False SERVO_SIMULATED_IN = 'servo_in.txt' SERVO_SIMULATED_OUT = 'servo_out.txt' REMOTE_SIMULATED_IN = 'remote_in.txt' REMOTE_SIMULATED_OUT = 'remote_out.txt' RASPBERRY_PI = psutil.sys.platform.title() == 'Linux' if RASPBERRY_PI: ARDUINO_LOG = messageboard.MESSAGEBOARD_PATH + ARDUINO_LOG SERIALS_LOG = messageboard.MESSAGEBOARD_PATH + SERIALS_LOG SERVO_SIMULATED_OUT = messageboard.MESSAGEBOARD_PATH + SERVO_SIMULATED_OUT SERVO_SIMULATED_IN = messageboard.MESSAGEBOARD_PATH + SERVO_SIMULATED_IN REMOTE_SIMULATED_OUT = messageboard.MESSAGEBOARD_PATH + REMOTE_SIMULATED_OUT REMOTE_SIMULATED_IN = messageboard.MESSAGEBOARD_PATH + REMOTE_SIMULATED_IN ARDUINO_ROLLING_LOG = messageboard.WEBSERVER_PATH + ARDUINO_ROLLING_LOG CONNECTION_FLAG_BLUETOOTH = 1 CONNECTION_FLAG_USB = 2 CONNECTION_FLAG_SIMULATED = 3 RASPBERRY_PI = psutil.sys.platform.title() == 'Linux' SN_SERVO = '5583834303435111C1A0' SERVO_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (2, '98:D3:11:FC:42:16', 1)) SN_REMOTE = '75835343130351802272' REMOTE_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (1, '98:D3:91:FD:B3:C9', 1)) MIN_ALTITUDE = 5 # below this elevation degrees, turn off the tracking if SIMULATE_ARDUINO: SERVO_CONNECTION = (CONNECTION_FLAG_SIMULATED, (SERVO_SIMULATED_IN, SERVO_SIMULATED_OUT)) REMOTE_CONNECTION = ( CONNECTION_FLAG_SIMULATED, (REMOTE_SIMULATED_IN, REMOTE_SIMULATED_OUT)) KEY_NOT_PRESENT_STRING = 'N/A' <----SKIPPED LINES----> if not format_string: format_string = self.read_format try: data = Read(self.link, format_string) except OSError as e: if self.error_pin: self.to_parent_q.put(('pin', (self.error_pin, True))) self.Reopen(log_message='Failed to read: %s' % e) return self.Read(format_string=format_string) self.last_read = time.time() if data: self.last_receipt = time.time() if LOG_SERIALS: ts = time.time() - self.start_time str_data = str(['%7.2f' % d if isinstance(d, float) else str(d) for d in data]) with open(SERIALS_LOG, 'a') as f: f.write('%10.3f RECD@%s: %s\n' % (ts, self.name, str_data)) if self.read_timeout and self.last_read - self.last_receipt > self.read_timeout: if self.error_pin: self.to_parent_q.put(('pin', (self.error_pin, True))) self.Reopen(log_message='Heartbeat not received in %.2f seconds' % (self.last_read - self.last_receipt)) return self.Read(format_string=format_string) return data def Write(self, values, format_string=None): """Writes to an open serial. Writes to an open serial values as identified in the format_string provided here, or if not provided in this call, as saved on the Serial instance. If an OSError exception is detected, this method will attempt to reopen the connection. Args: values: tuple of values to send matching that as identified in format_string. format_string: String of the form expected by struct.pack """ ts = time.time() - self.start_time str_values = str(['%7.2f' % v if isinstance(v, float) else str(v) for v in values]) if self.connection_type == CONNECTION_FLAG_SIMULATED: with open(self.connection_tuple[1], 'a') as f: f.write('%10.3f: %s\n' % (ts, str_values)) <----SKIPPED LINES----> Log('Process started with process id %d' % os.getpid()) # Ensures that the child can exit if the parent exits unexpectedly # docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue.cancel_join_thread to_arduino_q.cancel_join_thread() to_parent_q.cancel_join_thread() # write_format: azimuth, altitude, R, G, & B intensity # read heartbeat: millis link = Serial( *SERVO_CONNECTION, read_timeout=5, error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION, to_parent_q=to_parent_q, read_format='l', write_format='ffhhh', name='Servo') link.Open() last_flight = {} last_angles = (0, 0) flight, json_desc_dict, configuration, additional_attr = InitialMessageValues( to_arduino_q) next_read = time.time() + READ_DELAY_TIME next_write = time.time() + WRITE_DELAY_TIME now = GetNow(json_desc_dict, additional_attr) flight_present = False while not shutdown.value: if not to_arduino_q.empty(): flight, json_desc_dict, configuration, additional_attr = to_arduino_q.get( block=False) if 'test_servos' in configuration: messageboard.RemoveSetting(configuration, 'test_servos') link.Write((0, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((90, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((180, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((270, 0, *LASER_RGB_OFF)) new_flight = DifferentFlights(flight, last_flight) if new_flight: <----SKIPPED LINES----> line1_decimal_mask = '00000000' line2_decimal_mask = '00000000' if display_mode == DISP_LAST_FLIGHT_NUMB_ORIG_DEST: # UAL1827 / SFO-LAX line1 = '' line2 = '' if flight: line1 = messageboard.DisplayFlightNumber(flight) origin = messageboard.DisplayOriginIata(flight)[:3] destination = messageboard.DisplayDestinationIata(flight)[:3] line2 = '%s-%s' % (origin, destination) elif display_mode == DISP_LAST_FLIGHT_AZIMUTH_ELEVATION: # AZM+193.1 / ALT +79.3 current_angles = AzimuthAltitude(flight, GetNow(json_desc_dict, additional_attr)) line1 = '' line2 = '' if flight: if current_angles: (azimuth, altitude) = current_angles line1 = 'AZM%s' % FloatToAlphanumericStr(azimuth, 1, 5, True) line1_decimal_mask = '00000010' line2 = 'ALT%s' % FloatToAlphanumericStr(altitude, 1, 5, True) line2_decimal_mask = '00000010' else: line1 = KEY_NOT_PRESENT_STRING line2 = KEY_NOT_PRESENT_STRING elif display_mode == DISP_FLIGHT_COUNT_LAST_SEEN: # 18 TODAY / T+ 14H line1 = '%2d TODAY' % json_desc_dict.get('flight_count_today', 0) elapsed_time_str = 'UNK' if flight_last_seen: elapsed_time_str = SecondsToShortString( GetNow(json_desc_dict, additional_attr) - flight_last_seen) line2 = 'T+ %s' % elapsed_time_str elif display_mode == DISP_RADIO_RANGE: # RNG 87MI / 9 PLANES radio_range = json_desc_dict.get('radio_range_miles') radio_range_str = KEY_NOT_PRESENT_STRING if radio_range is not None: radio_range_str = '%2dMI' % round(radio_range) line1 = 'RNG %s' % radio_range_str radio_range_flights = json_desc_dict.get('radio_range_flights', 0) plural = '' if radio_range_flights != 1: plural = 'S' line2 = '%d PLANE%s' % (radio_range_flights, plural) d = {} setting_screen_enabled = False if 'setting_screen_enabled' in configuration: setting_screen_enabled = True d['setting_screen_enabled'] = setting_screen_enabled d['setting_max_distance'] = configuration['setting_max_distance'] d['setting_max_altitude'] = configuration['setting_max_altitude'] d['setting_on_time'] = configuration['setting_on_time'] d['setting_off_time'] = configuration['setting_off_time'] d['setting_delay'] = configuration['setting_delay'] d['line1'] = line1 d['line2'] = line2 d['line1_dec_mask'] = int(line1_decimal_mask, 2) d['line2_dec_mask'] = int(line2_decimal_mask, 2) return d def ExecuteArduinoCommand( command, configuration, display_mode, low_battery, to_parent_q, link): """Executes the request as communicated in the command string. The remote may make one of the following requests: - Update a setting - (Re)display a recent flight - Display a histogram - Send information for a different display mode - Indicate that the battery is low Args: command: dictionary representing all data fields from remote. configuration: dictionary representing the current state of the messageboard configuration. display_mode: current display mode; only passed so that we may identify changes. low_battery: current battery status; only passed so that we may identify changes. to_parent_q: multiprocessing queue, where instructions to send back to messageboard, if any, can be placed. link: the open serial link. Returns: A 3-tuple of potentially-updated display_mode, low_battery, the updated configuration dictionary. """ # command might update a setting; see if there's a change, and if so, write to disk setting_change = False # remote sees T/F whereas messageboard.py & the web interface expect 'on'/absent key if command['setting_screen_enabled_bool']: command['setting_screen_enabled'] = 'on' log_lines = [] setting_keys = ['setting_screen_enabled', 'setting_max_distance', 'setting_max_altitude', 'setting_on_time', 'setting_off_time', 'setting_delay'] for key in setting_keys: if command.get(key) != configuration.get(key): log_lines.append(' |-->Setting %s updated from %s to %s' % ( key, str(configuration[key]), str(command[key]))) setting_change = True configuration[key] = command[key] if setting_change: settings_string = messageboard.BuildSettings(configuration) to_parent_q.put(('update_configuration', (settings_string, ))) <----SKIPPED LINES----> to_parent_q.put(('histogram', (h_type, h_history))) log_lines.append(' |-->Requested %s histogram with %s data' % (h_type, h_history)) # a command might update us on the display mode; based on the display mode, we might # pass different attributes back to the remote if display_mode != command['display_mode']: log_lines.append(' |-->Display mode set to %d (%s)' % ( command['display_mode'], DISPLAY_MODE_NAMES[display_mode])) # a command might tell us the battery is low to_parent_q.put( ('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, command['low_battery']))) if low_battery != command['low_battery']: log_lines.append(' |-->Low battery set to %d' % command['low_battery']) if VERBOSE: if log_lines: log_lines.insert(0, '') # for improved formatting Log('\n'.join(log_lines), link) return command['display_mode'], command['low_battery'], configuration, setting_change def SecondsToShortString(seconds): """Converts a number of seconds to a three-character time representation (i.e.: 87M). Converts seconds to a three character representation containing at most two digits (1-99) and one character indicating time unit (S, M, H, or D). Args: seconds: Number of seconds. Returns: String as described. """ s = round(seconds) if s < 99: return '%2dS' % s m = round(seconds / messageboard.SECONDS_IN_MINUTE) if m < 99: return '%2dM' % m h = round(seconds / messageboard.SECONDS_IN_HOUR) if h < 99: return '%2dH' % h d = round(seconds / (messageboard.SECONDS_IN_DAY)) return '%2dD' % d def SimulateCommand(potential_commands, counter, fraction_command=0.01, randomized=False): """Simulates the remote generating a command for remote-free testing. A command from the list of potential_commands is generated periodically, roughly fraction_command percent of the time. - not randomized: a command is returned every time (counter / fraction_command) rolls over to a new integer. The command returned is the next one in the list of potential commands. For instance, if fraction_command = 0.01, when counter=100, the first command in potential_commands is returned; when counter=200, the second command is returned, and so on. At the end of the list, we rotate back to the first command. - randomized: fraction_command percent of the time, a randomly selected command is sent. Args: potential_commands: A fully-formed string (potentially including the ack key-value pair) to sent to the remote. counter: integer indicating how many times a command could potentially have been generated; only relevant if randomized is False. fraction_command: The fraction (or percent) of the time a command is returned. <----SKIPPED LINES----> return t[1] raise ValueError('%d is not in %s' % (n, mapping)) def SplitFormat(config): """From a tuple describing the format, returns keys and format. Transforms a easy-to-read format specifier of, for example: (('key1', '?'), ('key2', '?'), ('key3', 'h')) into a tuple of the keys and formats in the same sequence as provided: ('key1', 'key2', 'key3') ('?', '?', 'h') and a string format specifier: '??h' """ k_tuple = tuple([t[0] for t in config]) f_tuple = tuple([t[1] for t in config]) f_string = ''.join(f_tuple) # The initial character of = disables alignment padding # i.e.: https://docs.python.org/3/library/struct.html#struct-alignment f_string = '=' + f_string return k_tuple, f_tuple, f_string def RemoteMain(to_arduino_q, to_parent_q, shutdown): """Main process for controlling the arduino-based remote control. Takes various data from the messageboard and formats it for display on the alphanumeric display on the remote control; provides latest configuration; and executes any commands such as histogram requests or setting updates. """ sys.stderr = open(messageboard.STDERR_FILE, 'a') Log('Process started with process id %d' % os.getpid()) # Ensures that the child can exit if the parent exits unexpectedly # docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue.cancel_join_thread to_arduino_q.cancel_join_thread() to_parent_q.cancel_join_thread() #pylint: disable = bad-whitespace read_config = ( # when confirmed is true, this is a command; when false, this is a heartbeat ('confirmed', '?'), ('setting_screen_enabled_bool', '?'), ('setting_max_distance', 'h'), ('setting_max_altitude', 'l'), ('setting_on_time', 'h'), ('setting_off_time', 'h'), ('setting_delay', 'h'), ('last_plane', '?'), ('display_mode', 'h'), ('histogram_enabled', '?'), ('current_hist_type', 'h'), ('current_hist_history', 'h'), ('low_battery', '?')) write_config = ( ('setting_screen_enabled', '?'), ('setting_max_distance', 'h'), ('setting_max_altitude', 'l'), ('setting_on_time', 'h'), ('setting_off_time', 'h'), ('setting_delay', 'h'), ('line1', '8s'), ('line2', '8s'), ('line1_dec_mask', 'h'), ('line2_dec_mask', 'h')) #pylint: enable = bad-whitespace read_keys, unused_read_format_tuple, read_format_string = SplitFormat(read_config) write_keys, write_format_tuple, write_format_string = SplitFormat(write_config) values_d = {} low_batt = False to_parent_q.put(('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, low_batt))) link = Serial( *REMOTE_CONNECTION, read_timeout=5, error_pin=messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, to_parent_q=to_parent_q, read_format=read_format_string, write_format=write_format_string, name='Remote') link.Open() display_mode = 0 flight, json_desc_dict, configuration, additional_attr = InitialMessageValues( to_arduino_q) next_read = time.time() + READ_DELAY_TIME next_write = time.time() + WRITE_DELAY_TIME ignore_parent_configuration = False updated_configuration = configuration while not shutdown.value: if not to_arduino_q.empty(): to_arduino_message = to_arduino_q.get(block=False) flight, json_desc_dict, configuration, additional_attr = to_arduino_message # There is a tricky parallel process timing issues: the remote has more up-to-date # configuration for a brief interval until the parent has chance to write the # configuration; all already-queued messages (perhaps at most one) will still have # the old configuration prior to any updates from the remote. Thus, we need to # use the updated configuration from the remote, ignoring the configuration from # parent until such time as they match. if configuration == updated_configuration: ignore_parent_configuration = False else: Log('REMOTE: Settings from main out of date; ignoring') if ignore_parent_configuration: configuration = updated_configuration if time.time() >= next_write: message_dict = GenerateRemoteMessage( flight, json_desc_dict, configuration, additional_attr, display_mode) message_tuple = DictToValueTuple(message_dict, write_keys, write_format_tuple) link.Write(message_tuple) next_write = time.time() + WRITE_DELAY_TIME if time.time() >= next_read: values_t = link.Read() # simple ack message sent by servos values_d = dict(zip(read_keys, values_t)) if values_d.get('confirmed'): results = ExecuteArduinoCommand( values_d, configuration, display_mode, low_batt, to_parent_q, link) (display_mode, low_batt, updated_configuration, ignore_parent_configuration) = results next_read = time.time() + READ_DELAY_TIME link.Close() Log('Shutdown signal received by process %d' % os.getpid(), link) to_parent_q.put(('pin', (messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, True))) |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
123456789101112131415161718192021222324252627282930313233 3435363738394041424344454647484950515253545556575859606162 281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924 925926927928929930931932933934935936937938939940941942943944 958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992 993 994 99599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046 10931094109510961097109810991100110111021103110411051106110711081109111011111112 111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199 120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223 1224122512261227122812291230 12311232123312341235 | #!/usr/bin/python3 import numbers import os import random import struct import subprocess import sys import termios import time import psutil import serial import serial.tools.list_ports from pySerialTransfer import pySerialTransfer from constants import RASPBERRY_PI, MESSAGEBOARD_PATH, WEBSERVER_PATH import messageboard ARDUINO_LOG = 'arduino_log.txt' ARDUINO_ROLLING_LOG = 'arduino_rolling_log.txt' SERIALS_LOG = 'arduino_serials_log.txt' VERBOSE = False # log additional fine-grained details into ARDUINO_LOG LOG_SERIALS = False # log serial data sent from Arduino to ARDUINO_LOG SIMULATE_ARDUINO = False SERVO_SIMULATED_IN = 'servo_in.txt' SERVO_SIMULATED_OUT = 'servo_out.txt' REMOTE_SIMULATED_IN = 'remote_in.txt' REMOTE_SIMULATED_OUT = 'remote_out.txt' if RASPBERRY_PI: ARDUINO_LOG = MESSAGEBOARD_PATH + ARDUINO_LOG SERIALS_LOG = MESSAGEBOARD_PATH + SERIALS_LOG SERVO_SIMULATED_OUT = MESSAGEBOARD_PATH + SERVO_SIMULATED_OUT SERVO_SIMULATED_IN = MESSAGEBOARD_PATH + SERVO_SIMULATED_IN REMOTE_SIMULATED_OUT = MESSAGEBOARD_PATH + REMOTE_SIMULATED_OUT REMOTE_SIMULATED_IN = MESSAGEBOARD_PATH + REMOTE_SIMULATED_IN ARDUINO_ROLLING_LOG = WEBSERVER_PATH + ARDUINO_ROLLING_LOG CONNECTION_FLAG_BLUETOOTH = 1 CONNECTION_FLAG_USB = 2 CONNECTION_FLAG_SIMULATED = 3 RASPBERRY_PI = psutil.sys.platform.title() == 'Linux' SN_SERVO = '5583834303435111C1A0' SERVO_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (2, '98:D3:11:FC:42:16', 1)) SN_REMOTE = '75835343130351802272' REMOTE_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (1, '98:D3:91:FD:B3:C9', 1)) MIN_ALTITUDE = 5 # below this elevation degrees, turn off the tracking if SIMULATE_ARDUINO: SERVO_CONNECTION = (CONNECTION_FLAG_SIMULATED, (SERVO_SIMULATED_IN, SERVO_SIMULATED_OUT)) REMOTE_CONNECTION = ( CONNECTION_FLAG_SIMULATED, (REMOTE_SIMULATED_IN, REMOTE_SIMULATED_OUT)) KEY_NOT_PRESENT_STRING = 'N/A' <----SKIPPED LINES----> if not format_string: format_string = self.read_format try: data = Read(self.link, format_string) except OSError as e: if self.error_pin: self.to_parent_q.put(('pin', (self.error_pin, True))) self.Reopen(log_message='Failed to read: %s' % e) return self.Read(format_string=format_string) self.last_read = time.time() if data: self.last_receipt = time.time() if LOG_SERIALS: ts = time.time() - self.start_time str_data = str(['%7.2f' % d if isinstance(d, float) else str(d) for d in data]) with open(SERIALS_LOG, 'a') as f: f.write('%10.3f RECD@%s: %s\n' % (ts, self.name, str_data)) if self.read_timeout and self.last_read - self.last_receipt > self.read_timeout: if self.error_pin: self.to_parent_q.put(('pin', (self.error_pin, True))) self.Reopen(log_message='Heartbeat not received in %.2f seconds (expected: %.2f)' % (self.last_read - self.last_receipt, self.read_timeout)) return self.Read(format_string=format_string) return data def Write(self, values, format_string=None): """Writes to an open serial. Writes to an open serial values as identified in the format_string provided here, or if not provided in this call, as saved on the Serial instance. If an OSError exception is detected, this method will attempt to reopen the connection. Args: values: tuple of values to send matching that as identified in format_string. format_string: String of the form expected by struct.pack """ ts = time.time() - self.start_time str_values = str(['%7.2f' % v if isinstance(v, float) else str(v) for v in values]) if self.connection_type == CONNECTION_FLAG_SIMULATED: with open(self.connection_tuple[1], 'a') as f: f.write('%10.3f: %s\n' % (ts, str_values)) <----SKIPPED LINES----> Log('Process started with process id %d' % os.getpid()) # Ensures that the child can exit if the parent exits unexpectedly # docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue.cancel_join_thread to_arduino_q.cancel_join_thread() to_parent_q.cancel_join_thread() # write_format: azimuth, altitude, R, G, & B intensity # read heartbeat: millis link = Serial( *SERVO_CONNECTION, read_timeout=5, error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION, to_parent_q=to_parent_q, read_format='l', write_format='ffhhh', name='Servo') link.Open() last_flight = {} last_angles = (0, 0) flight, json_desc_dict, configuration, additional_attr = InitialMessageValues( to_arduino_q) next_read = 0 next_write = 0 now = GetNow(json_desc_dict, additional_attr) flight_present = False while not shutdown.value: if not to_arduino_q.empty(): flight, json_desc_dict, configuration, additional_attr = to_arduino_q.get( block=False) if 'test_servos' in configuration: messageboard.RemoveSetting(configuration, 'test_servos') link.Write((0, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((90, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((180, 0, *LASER_RGB_OFF)) time.sleep(1) link.Write((270, 0, *LASER_RGB_OFF)) new_flight = DifferentFlights(flight, last_flight) if new_flight: <----SKIPPED LINES----> line1_decimal_mask = '00000000' line2_decimal_mask = '00000000' if display_mode == DISP_LAST_FLIGHT_NUMB_ORIG_DEST: # UAL1827 / SFO-LAX line1 = '' line2 = '' if flight: line1 = messageboard.DisplayFlightNumber(flight) origin = messageboard.DisplayOriginIata(flight)[:3] destination = messageboard.DisplayDestinationIata(flight)[:3] line2 = '%s-%s' % (origin, destination) elif display_mode == DISP_LAST_FLIGHT_AZIMUTH_ELEVATION: # AZM+193.1 / ALT +79.3 current_angles = AzimuthAltitude(flight, GetNow(json_desc_dict, additional_attr)) line1 = '' line2 = '' if flight: if current_angles: (azimuth, altitude) = current_angles line1 = 'AZM%s' % FloatToAlphanumericStr(azimuth, 1, 6, True) line1_decimal_mask = '00000010' line2 = 'ALT%s' % FloatToAlphanumericStr(altitude, 1, 6, True) line2_decimal_mask = '00000010' else: line1 = KEY_NOT_PRESENT_STRING line2 = KEY_NOT_PRESENT_STRING elif display_mode == DISP_FLIGHT_COUNT_LAST_SEEN: # 18 TODAY / T+ 14H flight_count_today = additional_attr.get('flight_count_today', 0) flight_count_today = ('%2d' % flight_count_today).ljust(3) line1 = '%sTODAY' % flight_count_today elapsed_time_str = 'UNK' if flight_last_seen: elapsed_time_str, partial_dec_mask = SecondsToShortString( GetNow(json_desc_dict, additional_attr) - flight_last_seen) line2 = 'T+ %s' % elapsed_time_str line2_decimal_mask = '000' + partial_dec_mask + '00' elif display_mode == DISP_RADIO_RANGE: # RNG 87MI / 9 PLANES radio_range = json_desc_dict.get('radio_range_miles') radio_range_str = KEY_NOT_PRESENT_STRING if radio_range is not None: radio_range_str = '%2dMI' % round(radio_range) line1 = 'RNG %s' % radio_range_str radio_range_flights = json_desc_dict.get('radio_range_flights', 0) plural = '' if radio_range_flights != 1: plural = 'S' line2 = '%d PLANE%s' % (radio_range_flights, plural) d = {} setting_screen_enabled = False if 'setting_screen_enabled' in configuration: setting_screen_enabled = True d['setting_screen_enabled'] = setting_screen_enabled d['setting_max_distance'] = configuration['setting_max_distance'] d['setting_max_altitude'] = configuration['setting_max_altitude'] d['setting_on_time'] = configuration['setting_on_time'] d['setting_off_time'] = configuration['setting_off_time'] d['setting_delay'] = configuration['setting_delay'] d['line1'] = line1.upper() d['line2'] = line2.upper() d['line1_dec_mask'] = int(line1_decimal_mask, 2) d['line2_dec_mask'] = int(line2_decimal_mask, 2) return d def ExecuteArduinoCommand( command, configuration, display_mode, low_battery, to_parent_q, link): """Executes the request as communicated in the command string. The remote may make one of the following requests: - Update a setting - (Re)display a recent flight - Display a histogram - Send information for a different display mode - Indicate that the battery is low Args: command: dictionary representing all data fields from remote. configuration: dictionary representing the current state of the messageboard configuration. display_mode: current display mode; only passed so that we may identify changes. low_battery: current battery status; only passed so that we may identify changes. to_parent_q: multiprocessing queue, where instructions to send back to messageboard, if any, can be placed. link: the open serial link. Returns: A 2-tuple of potentially-updated display_mode, and low_battery. """ # command might update a setting; see if there's a change, and if so, write to disk setting_change = False # remote sees T/F whereas messageboard.py & the web interface expect 'on'/absent key if command['setting_screen_enabled_bool']: command['setting_screen_enabled'] = 'on' log_lines = [] setting_keys = ['setting_screen_enabled', 'setting_max_distance', 'setting_max_altitude', 'setting_on_time', 'setting_off_time', 'setting_delay'] for key in setting_keys: if command.get(key) != configuration.get(key): log_lines.append(' |-->Setting %s updated from %s to %s' % ( key, str(configuration[key]), str(command[key]))) setting_change = True configuration[key] = command[key] if setting_change: settings_string = messageboard.BuildSettings(configuration) to_parent_q.put(('update_configuration', (settings_string, ))) <----SKIPPED LINES----> to_parent_q.put(('histogram', (h_type, h_history))) log_lines.append(' |-->Requested %s histogram with %s data' % (h_type, h_history)) # a command might update us on the display mode; based on the display mode, we might # pass different attributes back to the remote if display_mode != command['display_mode']: log_lines.append(' |-->Display mode set to %d (%s)' % ( command['display_mode'], DISPLAY_MODE_NAMES[display_mode])) # a command might tell us the battery is low to_parent_q.put( ('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, command['low_battery']))) if low_battery != command['low_battery']: log_lines.append(' |-->Low battery set to %d' % command['low_battery']) if VERBOSE: if log_lines: log_lines.insert(0, '') # for improved formatting Log('\n'.join(log_lines), link) return command['display_mode'], command['low_battery'] def SecondsToShortString(s): """Converts a number of seconds to a three-character time representation (i.e.: 23M). Converts seconds to a three character representation containing at most two digits, potentially with a decimal point, and one character indicating time unit (S, M, H, or D). Args: s: Number of seconds. Returns: 2-tuple of string as described and string map indicating decimal position """ m = s / messageboard.SECONDS_IN_MINUTE h = s / messageboard.SECONDS_IN_HOUR d = s / messageboard.SECONDS_IN_DAY no_decimals = '000' decimal_after_first_character = '100' partial_decimal_mask = no_decimals if s < 10: numeric_string = '%sS' % FloatToAlphanumericStr(s, 1, 3, sign=False) partial_decimal_mask = decimal_after_first_character elif s < messageboard.SECONDS_IN_MINUTE: numeric_string = '%2dS' % round(s) elif m < 10: numeric_string = '%sM' % FloatToAlphanumericStr(m, 1, 3, sign=False) partial_decimal_mask = decimal_after_first_character elif m < messageboard.MINUTES_IN_HOUR: numeric_string = '%2dM' % round(m) elif h < 10: numeric_string = '%sH' % FloatToAlphanumericStr(h, 1, 3, sign=False) partial_decimal_mask = decimal_after_first_character elif h < messageboard.HOURS_IN_DAY: numeric_string = '%2dH' % round(h) elif d < 10: numeric_string = '%sD' % FloatToAlphanumericStr(d, 1, 3, sign=False) partial_decimal_mask = decimal_after_first_character else: numeric_string = '%2dD' % round(d) return numeric_string, partial_decimal_mask def SimulateCommand(potential_commands, counter, fraction_command=0.01, randomized=False): """Simulates the remote generating a command for remote-free testing. A command from the list of potential_commands is generated periodically, roughly fraction_command percent of the time. - not randomized: a command is returned every time (counter / fraction_command) rolls over to a new integer. The command returned is the next one in the list of potential commands. For instance, if fraction_command = 0.01, when counter=100, the first command in potential_commands is returned; when counter=200, the second command is returned, and so on. At the end of the list, we rotate back to the first command. - randomized: fraction_command percent of the time, a randomly selected command is sent. Args: potential_commands: A fully-formed string (potentially including the ack key-value pair) to sent to the remote. counter: integer indicating how many times a command could potentially have been generated; only relevant if randomized is False. fraction_command: The fraction (or percent) of the time a command is returned. <----SKIPPED LINES----> return t[1] raise ValueError('%d is not in %s' % (n, mapping)) def SplitFormat(config): """From a tuple describing the format, returns keys and format. Transforms a easy-to-read format specifier of, for example: (('key1', '?'), ('key2', '?'), ('key3', 'h')) into a tuple of the keys and formats in the same sequence as provided: ('key1', 'key2', 'key3') ('?', '?', 'h') and a string format specifier: '??h' """ k_tuple = tuple([t[0] for t in config]) f_tuple = tuple([t[1] for t in config]) f_string = ''.join(f_tuple) # https://docs.python.org/3/library/struct.html#struct-alignment f_string = '<' + f_string return k_tuple, f_tuple, f_string def SendRemoteMessage( flight, json_desc_dict, configuration, additional_attr, display_mode, write_keys, write_format_tuple, link): """Sends a message to the remote with current settings & text for given mode.""" message_dict = GenerateRemoteMessage( flight, json_desc_dict, configuration, additional_attr, display_mode) message_tuple = DictToValueTuple(message_dict, write_keys, write_format_tuple) link.Write(message_tuple) next_write = time.time() + WRITE_DELAY_TIME return next_write def RemoteMain(to_arduino_q, to_parent_q, shutdown): """Main process for controlling the arduino-based remote control. Takes various data from the messageboard and formats it for display on the alphanumeric display on the remote control; provides latest configuration; and executes any commands such as histogram requests or setting updates. """ sys.stderr = open(messageboard.STDERR_FILE, 'a') Log('Process started with process id %d' % os.getpid()) # Ensures that the child can exit if the parent exits unexpectedly # docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue.cancel_join_thread to_arduino_q.cancel_join_thread() to_parent_q.cancel_join_thread() #pylint: disable = bad-whitespace read_config = ( # when confirmed is true, this is a command; when false, this is a heartbeat ('confirmed', '?'), ('setting_screen_enabled_bool', '?'), ('setting_max_distance', 'H'), ('setting_max_altitude', 'L'), ('setting_on_time', 'H'), ('setting_off_time', 'H'), ('setting_delay', 'H'), ('last_plane', '?'), ('display_mode', 'H'), ('histogram_enabled', '?'), ('current_hist_type', 'H'), ('current_hist_history', 'H'), ('low_battery', '?')) write_config = ( ('setting_screen_enabled', '?'), # 1 bytes ('setting_max_distance', 'H'), # 2 bytes ('setting_max_altitude', 'L'), # 4 bytes ('setting_on_time', 'H'), # 2 bytes ('setting_off_time', 'H'), # 2 bytes ('setting_delay', 'H'), # 2 bytes ('line1', '9s'), # 9 bytes; 8 character plus terminator ('line2', '9s'), # 9 bytes; 8 character plus terminator ('line1_dec_mask', 'H'), # 2 bytes ('line2_dec_mask', 'H')) # 2 bytes #pylint: enable = bad-whitespace read_keys, unused_read_format_tuple, read_format_string = SplitFormat(read_config) write_keys, write_format_tuple, write_format_string = SplitFormat(write_config) values_d = {} low_batt = False to_parent_q.put(('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, low_batt))) link = Serial( *REMOTE_CONNECTION, read_timeout=5, error_pin=messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, to_parent_q=to_parent_q, read_format=read_format_string, write_format=write_format_string, name='Remote') link.Open() display_mode = 2 #TODO flight, json_desc_dict, configuration, additional_attr = InitialMessageValues( to_arduino_q) next_read = 0 next_write = 0 while not shutdown.value: if not to_arduino_q.empty(): to_arduino_message = to_arduino_q.get(block=False) flight, json_desc_dict, configuration, additional_attr = to_arduino_message if 'test_remote' in configuration: messageboard.RemoveSetting(configuration, 'test_remote') def TestDisplayMode(m): SendRemoteMessage( flight, json_desc_dict, configuration, additional_attr, m, write_keys, write_format_tuple, link) time.sleep(1) TestDisplayMode(DISP_LAST_FLIGHT_NUMB_ORIG_DEST) TestDisplayMode(DISP_LAST_FLIGHT_AZIMUTH_ELEVATION) TestDisplayMode(DISP_FLIGHT_COUNT_LAST_SEEN) TestDisplayMode(DISP_RADIO_RANGE) if time.time() >= next_write: next_write = SendRemoteMessage( flight, json_desc_dict, configuration, additional_attr, display_mode, write_keys, write_format_tuple, link) if time.time() >= next_read: values_t = link.Read() # simple ack message sent by servos values_d = dict(zip(read_keys, values_t)) if values_d.get('confirmed'): display_mode, low_batt = ExecuteArduinoCommand( values_d, configuration, display_mode, low_batt, to_parent_q, link) next_read = time.time() + READ_DELAY_TIME link.Close() Log('Shutdown signal received by process %d' % os.getpid(), link) to_parent_q.put(('pin', (messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, True))) |