arduino-2020-05-28-1625.py
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)))