arduino-2020-06-15-1139.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789
1234567891011121314151617 1819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 686970717273747576777879808182838485868788








979899100101102103104105106107108109110111112113114115116 117118119120121122123124125126 127128129 130 131132133134135136137138139140141142143144145146147148149 150151152153154155156157158159160161162163 164165166167168 169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 223224225226227228229230231 232233234235236237238239240241242243244 245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 274275276277278279280281282283284285286 287288289290291292293294295296297298299300301302303304305306 307308309310   311312313314315316317318319320321322323324325 326327328 329330331 332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 388389390 391392393394395396397398399400401402403404405 406407408409410411412413414415416417418419420421422423424425426427 428429430431432433434 435436437438 439440441442443444445446447448449450451452 453 454455456457458459460461 462463464465466467468469470471472473474475476477478479480481








501502503504505506507508509510511512513514515516517518519520521522523524  525526527528529530531532533534535536537538539540541542543544545 546547548549550551552553554555 556557558559560561562563564565566567568569570571572 573574575576577 578 579580 581582583584 585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651 652653654655656657658659 660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710








736737738739740741742743744745746747748749750751752753754755756757 758759760761762763764765 766767768769770771772773774775776777778779780781782783 784785786787788789790791792793794795796797798 799800801802 803804 805806807808809810811812813814815816817 818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874 875876877878879880881882883884885886887888889890891892893894








926927928929930931932933934935936937938939940941942943944945946947948 949 950951952953954955956957958959960961962963964965966967968969970971972973974975 976977978979980981982983984985986987988989990 991992993 9949959969979989991000100110021003 10041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 10301031103210331034103510361037103810391040104110421043104410451046104710481049








10531054105510561057105810591060106110621063106410651066106710681069107010711072 10731074107510761077107810791080 10811082 1083 1084108510861087108810891090 1091109210931094109510961097109810991100110111021103 1104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185 1186118711881189119011911192 119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228 1229 12301231123212331234123512361237 1238 12391240124112421243124412451246124712481249125012511252125312541255 125612571258125912601261126212631264126512661267126812691270127112721273127412751276








#!/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, SHUTDOWN_TEXT
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

REMOTE_DISPLAY_MODE = 'display_mode.txt'

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
  REMOTE_DISPLAY_MODE = MESSAGEBOARD_PATH + REMOTE_DISPLAY_MODE

  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))  # with MOSFET
#SERVO_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (3, '98:D3:91:FD:B1:8F', 1))  # no MOSFET

SN_REMOTE = '75835343130351802272'
# directly connected to Serial2
# connected thru MOSFET to Serial1
REMOTE_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (1, '98:D3:91:FD:B3:C9', 1))

LASER_OFF = (False, False, False)
LASER_ALL = (True, True, True)
LASER_RED = (True, False, False)
LASER_GREEN = (False, True, False)
LASER_BLUE = (False, False, True)

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'

DISP_LAST_FLIGHT_NUMB_ORIG_DEST = 0
DISP_LAST_FLIGHT_AZIMUTH_ELEVATION = 1
DISP_FLIGHT_COUNT_LAST_SEEN = 2
DISP_RADIO_RANGE = 3
DISPLAY_MODE_NAMES = [
    'LAST_FLIGHT_NUMB_ORIG_DEST', 'LAST_FLIGHT_AZIMUTH_ELEVATION',
    'FLIGHT_COUNT_LAST_SEEN', 'RADIO_RANGE']

WRITE_DELAY_TIME = 0.2  # write to arduino every n seconds
READ_DELAY_TIME = 0.1  # read from arduino every n seconds

HISTOGRAM_TYPES = (
    (0, 'hour'),
    (1, 'day_of_month'),
    (2, 'origin'),




                            <----SKIPPED LINES---->





HISTOGRAM_HISTORY = (
    (0, 'today'),
    (1, '24h'),
    (2, '7d'),
    (3, '30d'))


def Log(message, ser=None, file=ARDUINO_LOG):
  """Logs with additional serial-connection details if provided"""
  if ser:
    additional_text = str(ser)
  else:
    additional_text = 'ARDUINO'

  if file == ARDUINO_LOG:
    rolling = ARDUINO_ROLLING_LOG
  else:
    rolling = None


  messageboard.Log('%s: %s' % (additional_text, message), file=file, rolling=rolling)


class Serial():
  """Serial object that allows for different connection types to be easily swapped.

  The Serial class allows for a persistent connection over one of several types of
  serial connections: USB, bluetooth, and simulation via text file.  It fully
  encapsulates error management so that the details of reconnecting on errors are
  hidden, yet it does check for and recover from timeouts, dropped connections, etc.


  Note that to read to/from Arduino, the data sizes (in bytes) expected by struct
  must match that in C. However, there are some inconsistencies amongst identically-

  named types - for instance, a Python int is 4 bytes, whereas a C int is 2 bytes.

  The following mappings on types to use with a format string and C declarations
  is valid:
  - C bool (1 byte) to struct ?
  - C short (2 bytes) to struct h
  - C long (4 bytes) to struct l
  - C float (4 bytes) to struct f
  - C char[] (1 byte per character) to struct s
  Struct formats: https://docs.python.org/3/library/struct.html
  Arduino types: https://robotresearchlab.com/2016/11/14/variable-data-types/
  """
  def __init__(
      self, connection_type, connection_tuple, baud=9600, open_timeout=5,
      read_format=None, write_format=None, read_timeout=0, error_pin=None,
      to_parent_q=None, name=None):
    """Creates a new instance of the Serial class.

    Args:
      connection_type: This identifies whether we are connecting with bluetooth, usb,
        or via a simulated connection; based on this, we identify an opening method, which

        is then called as open_function(connection_tuple, baud=baud, timeout=open_timeout)
      connection_tuple: A tuple of connection details specific to how the serial object
        connects - i.e.: a mac address, or a port name, etc.
      baud: Baud rate for the connection.
      open_timeout: The number of seconds we should wait on opening a new connection
        before timing out.
      read_format: Since reads from a serial device are often using the same struct.pack
        format_string for all calls, this allows that to be instantiated once and then
        referenced as needed.
      write_format: Similar concept to read_format, but for sending messages to the
        serial device.
      read_timeout: If the serial device is providing a heartbeat, this accelerates
        the identification of a dropped connection by trying to reconnect as soon as the
        time difference between the last non-empty read, and the last read attempt,

        exceeds this many seconds. Note, however, that we may not necessarily be able
        to recover more quickly, based on timeouts in the underlying libraries and systems;
        this can be disabled by setting it to 0.
      error_pin: If set, the messageboard GPIO pin is set to high whenever we have a
        failed / non-open connection, and low whenever we believe we have reconnected.

      to_parent_q: message queue to send status updates to parent
      name: an optional text string used in some logging to help identify which
        serial connection the log message is associated with.
    """
    self.connection_type = connection_type
    self.connection_tuple = connection_tuple
    self.baud = baud
    self.open_timeout = open_timeout
    self.read_format = read_format
    self.write_format = write_format
    self.read_timeout = read_timeout
    self.error_pin = error_pin
    self.to_parent_q = to_parent_q
    self.last_read = 0
    self.last_receipt = 0
    self.reset_flag = True
    self.link = None
    self.name = name

    self.__simulated_reads__ = None

    if self.connection_type == CONNECTION_FLAG_BLUETOOTH:
      self.open_function = OpenBluetooth
    elif self.connection_type == CONNECTION_FLAG_USB:
      self.open_function = OpenUSB
    elif self.connection_type == CONNECTION_FLAG_SIMULATED:
      self.open_function = None

    if self.error_pin:  # Error turned on when main initiated; turned off when connected
      error_message = 'Process %s (%s) initialized into error state' % (
          os.getpid(), str(self.name))
      Log(error_message)
      self.to_parent_q.put(('pin', (self.error_pin, True, error_message)))

    self.start_time = time.time()

  def __str__(self):
    return self.name

  def Open(self):
    """Opens an instantiated serial connection for reading and writing."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      lines = []
      if os.path.exists(self.connection_tuple[0]):
        with open(self.connection_tuple[0], 'r') as f:
          for line in f:
            if line.strip():
              lines.append(eval(line))  # pylint: disable=W0123
      else:
        Log('File %s does not exist for simulated commands to Arudino'
            % self.connection_tuple[0], self.link)
      self.__simulated_reads__ = lines

      # clear out file so that shell tail -f process can continue to point to same file

      with open(self.connection_tuple[1], 'w') as f:
        f.write('')

      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, False)))
      return

    self.link = self.open_function(
        self.connection_tuple, name=self.name, baud=self.baud, timeout=self.open_timeout)

    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, False)))
    self.last_read = time.time()
    self.last_receipt = time.time()
    self.reset_flag = True

  def Reopen(self, log_message=None):
    """Closes and reopens a link, optionally logging a message."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')

    self.link = ReopenConnection(
        self.open_function, self.link, self.connection_tuple,

        name=self.name, baud=self.baud, timeout=self.open_timeout, log_message=log_message)
    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, False)))
    self.reset_flag = True
    self.last_read = time.time()
    self.last_receipt = time.time()

  def Close(self, close_message):
    """Closes an open serial connection."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      return

    self.link.close()
    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, True, close_message)))
    Log(close_message, self)

  def Available(self):
    """Calls self.link.available()."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')
    self.link.available()

  def Read(self, format_string=None, bytes_read=None):
    """Reads from an open serial.

    Reads from 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, or if this read, in failing to return non-empty results, means

    that the heartbeat timeout time has elapsed, this method will attempt to reopen
    the connection.

    Args:
      format_string: String of the form expected by struct.pack
      bytes_read: if passed, the bytes that are read are appended to the list.

    Returns:
      Tuple of values matching that as identified in format_string.
    """
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      if (
          self.__simulated_reads__ and

          time.time() - self.start_time > self.__simulated_reads__[0][0]):  # time for next
        next_line = self.__simulated_reads__.pop(0)
        return next_line[1]
      return ()

    if not format_string:
      format_string = self.read_format
    try:
      data = Read(self.link, format_string, bytes_read=bytes_read)
    except OSError as e:
      failure_message = 'Failed to read from %s: %s' % (self.name, e)
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      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:



      failure_message = 'Heartbeat not received in %.2f seconds (expected: %.2f) on %s' % (
          self.last_read - self.last_receipt, self.read_timeout, self.name)
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      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))
      return

    if not format_string:
      format_string = self.write_format
    try:
      Write(self.link, values, format_string)
    except OSError as e:
      failure_message = 'Failed to write: %s' % e
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      self.Write(values)
    if LOG_SERIALS:
      with open(SERIALS_LOG, 'a') as f:
        f.write('%10.3f SENT@%s: %s\n' % (ts, self.name, str_values))

  def HasReset(self):
    """Indicates exactly oncewhether the serial connection has reset since last called."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')

    flag = self.reset_flag
    self.reset_flag = False
    return flag


def RunCommand(cmd, sleep_seconds=1, log=True):
  """Runs shell command, checking if it completed (perhaps with errors) within timeout."""
  conn = subprocess.Popen(cmd, shell=True)
  time.sleep(sleep_seconds)
  conn.poll()

  if conn.returncode is None:
    Log('ERROR: %s did not complete within %d seconds'
        % (cmd, sleep_seconds))
    sys.exit()

  if log:
    Log('%s completed' % cmd)


def OpenBluetooth(connection_tuple, baud=9600, timeout=5, name=None):
  """Attempts to open bluetooth for a number of attempts, exiting program on fail.

  This may fail due to a missing /dev/rfcomm# entry and inability to create new one (or
  an existing one bound to same device number but pointing to a different serial
  connection), expectation of a handshake but lack of receipt of one, or timeouts
  in general before receipt.

  Args:
    connection_tuple: A 3-tuple of the rfcomm_device number (i.e.: the 1 in /dev/rfcomm1),
      mac_address of the bluetooth radio, and the bluetooth channel; all must be provided.

    baud: speed of the connection.
    timeout: seconds after a connection is established that this polls for a heartbeat
      before failing; a value of zero indicates no handshake needed.

    name: string name of the connection to display in error logging.

  Returns:
    An open pySerialTransfer.SerialTransfer link.
  """
  rfcomm_device, bt_mac_address, channel = connection_tuple

  attempts = 5  # max number of attempts to make a connection
  attempt = 1
  link = None
  dev = '/dev/rfcomm%d' % rfcomm_device
  if not name:
    name = '%s @ %s' % (bt_mac_address, dev)

  if not os.path.exists(dev):

    RunCommand('sudo rfcomm bind %d %s %d' % (rfcomm_device, bt_mac_address, channel))

  while attempt <= attempts:
    link = pySerialTransfer.SerialTransfer(dev, baud)

    # We think we made a connection; lets see if we can get a receipt
    start = time.time()
    if not timeout:
      return link
    try:
      while time.time() - start < timeout:
        b = ReadBytes(link)
        time.sleep(0.1)  # avoid a tight loop soaking up all serial / RPi resources
        if b:
          Log('Handshake received at %s by receipt of bytes %s' % (name, b))
          return link
      Log('No handshake received at %s after %d seconds on attempt %d'
          % (name, timeout, attempt))
    except OSError as e:
      Log('Handshake error with %s on attempt %d: %s' % (name, attempt, e))
    attempt += 1
  Log('ERROR: Failed to connect to %s after %d attempts' % (bt_mac_address, attempt - 1))

  sys.exit()


def OpenUSB(connection_tuple=('arduino', None), baud=9600, timeout=5):
  """Attempts to open USB for a number of seconds, exiting program on fail.

  This may fail due to lack of a plugged-in serial device matching the given definition.


  Args:
    connection_tuple: A 2-tuple of the manufacturer and the serial number of the expected
      device to connect with; either one may be missing (by providing a None value).

    baud: speed of the connection.
    timeout: seconds polling for the device matching the connection_tuple before fail
      on timeout.

  Raises:
    serial.SerialException: raised if no serial matching given attributes found

  Returns:
    An open pySerialTransfer.SerialTransfer link.
  """
  manufacturer, sn = connection_tuple
  initial_time = time.time()
  arduino_port = None
  attempted = False

  while not attempted or time.time() - initial_time < timeout and not arduino_port:

    attempted = True
    ports = serial.tools.list_ports.comports(include_links=False)
    for port in ports:

      port_mfg = port.manufacturer
      if port_mfg:
        port_mfg = port_mfg.lower()
      match_mfg = not manufacturer or (port_mfg and manufacturer.lower() in port_mfg)


      port_sn = port.serial_number
      match_sn = not sn or port_sn == sn

      if match_mfg and match_sn:
        arduino_port = port.device
        break

  link = None
  if arduino_port:
    link = pySerialTransfer.SerialTransfer(arduino_port, baud=baud)
    time.sleep(2)  # Need to give Arduino time before it is ready to receive
  else: # no USB-based matching port found
    raise serial.SerialException(
        'ERROR: No USB port found for mfg %s and sn %s' % (manufacturer, sn))

  return link


def ReopenConnection(




                            <----SKIPPED LINES---->




  """Reads the bytes at the link, returning a byte object."""
  read_bytes = []
  try:
    if link.available():
      if link.status < 0:
        raise serial.SerialException('ERROR: %s' % link.status)

      read_bytes = []
      for index in range(link.bytesRead):
        read_bytes.append(link.rxBuff[index])
  except (serial.SerialException, termios.error) as e:
    Log('Error in ReadBytes: %s' % e)
    read_bytes = []
  return read_bytes


def Unpack(read_bytes, format_string):
  """Unpacks a byte object into a tuple of values."""
  try:
    data = struct.unpack(format_string, bytearray(read_bytes))
  except struct.error as e:  # exception desc is missing some key detail, so re-raise
    raise struct.error(
        '%s but %d bytes provided (%s)' % (e, len(read_bytes), read_bytes))
  data = [str(d, 'ascii').split('\x00')[0] if isinstance(d, bytes) else d for d in data]


  return data


def Read(link, format_string, bytes_read=None):
  """Read and unpacks the bytes in one go."""
  read_bytes = ReadBytes(link)
  if isinstance(bytes_read, list):
    bytes_read.extend(read_bytes)
  data = ()
  if read_bytes:
    data = Unpack(read_bytes, format_string)
  return data


def AzimuthAltitude(flight, now):
  """Provides current best-estimate location details given last known position.

  Given a flight dictionary, this determines the plane's best estimate for current
  location using its last-known position in the flight's lat / lon / speed / altitude /
  and track. Those attributes may have already been updated by messageboard using a
  more recently obtained radio signal from dump1090 than that in the canonical location,

  and if so, key flight_loc_now indicates the time at which those locations are current
  as of.

  Args:
    flight: dictionary of flight attributes.
    now: epoch indicating the timestamp for which the azimuth and altitude should be
      calculated.

  Returns:
    Returns a tuple of the azimuth and altitude, in degrees, at the current system time.

  """
  if not flight:
    return None
  persistent_path = flight.get('persistent_path')
  if not persistent_path:
    return None

  most_recent_loc = persistent_path[-1]
  lat = most_recent_loc.get('lat')
  lon = most_recent_loc.get('lon')
  speed = most_recent_loc.get('speed')
  altitude = most_recent_loc.get('altitude')
  track = most_recent_loc.get('track')
  loc_now = most_recent_loc.get('now')
  unused_vert_rate = most_recent_loc.get('vert_rate')

  if not all([isinstance(x, numbers.Number) for x in (lat, lon, speed, altitude, track)]):

    return None

  elapsed_time = now - loc_now
  meters_traveled = messageboard.MetersTraveled(speed, elapsed_time)
  new_position = messageboard.TrajectoryLatLon((lat, lon), meters_traveled, track)

  unused_distance = messageboard.HaversineDistanceMeters(new_position, messageboard.HOME)


  # debugging messages to help identify whether servos are getting correct flight path info

  if elapsed_time < 60 and VERBOSE:
    Log('flight %s\n'
        'most_recent_loc: %s\n'
        'persistent path data: lat: %.5f; lon: %.5f; speed: %d; track: %d; altitude: %d\n'

        'now: %.5f\n'
        'loc_now: %.5f\n'
        'since measured data: elapsed_time: %.2f; '
        'meters_traveled: %.2f; new_position: %.5f, %.5f' % (
            messageboard.DisplayFlightNumber(flight),
            str(most_recent_loc),
            lat, lon, speed, track, altitude,
            now,
            loc_now,
            elapsed_time, meters_traveled, *new_position))

  angles = messageboard.Angles(
      messageboard.HOME,
      messageboard.HOME_ALT,
      new_position,
      altitude / messageboard.FEET_IN_METER)
  azimuth = angles['azimuth_degrees']
  altitude = angles['altitude_degrees']
  return (azimuth, altitude)

def DrainQueue(q):
  """Empties a queue, returning the last-retrieved value."""
  value = None
  while not q.empty():
    value = q.get(block=False)
  return value


def InitialMessageValues(q):
  """Initializes the arduino main processes with values from messageboard."""
  v = DrainQueue(q)
  if v:
    return v
  return {}, {}, {}, {}


def ServoTestOrdinal(link):
  """Point laser at each of 0, 90, 180, 270 and hold for a second with different colors."""
  link.Write((0, 0, *LASER_ALL))
  time.sleep(1)
  link.Write((90, 0, *LASER_RED))
  time.sleep(1)
  link.Write((180, 0, *LASER_GREEN))
  time.sleep(1)
  link.Write((270, 0, *LASER_BLUE))
  time.sleep(1)


def ServoTestSweep(link, altitude=45):
  """Sweep red laser around 360 degrees."""
  for azimuth in range(0, 360, 10):
    link.Write((azimuth, altitude, *LASER_RED))
    time.sleep(WRITE_DELAY_TIME)


def ServoMain(to_arduino_q, to_parent_q, shutdown):
  """Main servo controller for projecting the plane position on a hemisphere.

  Takes the latest flight from the to_arduino_q and converts that to the current
  azimuth and altitude of the plane on a hemisphere.
  """
  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()

  # write_format: azimuth, altitude, R, G, & B intensity
  # read heartbeat: millis
  link = Serial(
      *SERVO_CONNECTION, read_timeout=60,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION, to_parent_q=to_parent_q,

      read_format='l', write_format='ff???', 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)

  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_ordinal' in configuration:
        messageboard.RemoveSetting(configuration, 'test_servos_ordinal')
        ServoTestOrdinal(link)
      elif 'test_servos_sweep' in configuration:
        messageboard.RemoveSetting(configuration, 'test_servos_sweep')
        ServoTestSweep(link)

      new_flight = DifferentFlights(flight, last_flight)
      if new_flight:
        Log('Flight changed from %s to %s' % (
            messageboard.DisplayFlightNumber(last_flight),
            messageboard.DisplayFlightNumber(flight)
        ), ser=link)

        # Turn off laser so that line isn't traced while it moves to new position
        link.Write((*last_angles, *LASER_OFF))

      last_flight = flight

    if time.time() >= next_read:
      heartbeat = link.Read()  # simple ack message sent by servos
      next_read = time.time() + READ_DELAY_TIME
      if heartbeat and VERBOSE:
        Log(heartbeat)

    now = GetNow(json_desc_dict, additional_attr)

    current_angles = AzimuthAltitude(flight, now)
    if current_angles and time.time() > next_write:
      if current_angles[1] >= configuration['minimum_altitude_servo_tracking']:
        if VERBOSE:
          Log('Flight #: %s current_angles: %s' % (
              messageboard.DisplayFlightNumber(flight), str(current_angles)))
        laser_rgb = LaserRGBFlight(flight)
        link.Write((*current_angles, *laser_rgb))




                            <----SKIPPED LINES---->




  return LASER_BLUE


def DifferentFlights(f1, f2):
  """True if both squawk and flight number different; false otherwise."""
  if f1 is None and f2 is None:
    return True
  if f1 is None or f2 is None:
    return True

  if (
      f1.get('flight_number') != f2.get('flight_number')
      and f1.get('squawk') != f2.get('squawk')):
    return True
  return False


def FloatToAlphanumericStr(x, decimals, total_length, sign=True):
  """Formats a float as a string without a decimal point.

  Since the decimal point is controlled independently on the alphanumeric display,
  numbers that include decimals must be sent without the decimal point.  This formats

  a float as %+5.2f, for example - but without the decimal.

  Args:
    x: Value to format.
    decimals: how many digits should follow the (absent) decimal point.
    total_length: desired total length of the resulting string-ified number (inclusive
      of the absent decimal point.
    sign: boolean indicating whether a sign should be included for positive numbers.


  Returns:
    String as specified - for instance, 0.4 might be formatted as ' +04'.
  """
  sign_str = ''
  if sign:
    sign_str = '+'
  format_string = '%' + sign_str + str(total_length) + '.' + str(decimals) + 'f'
  number_string = format_string % x
  number_string = number_string.replace('.', '')
  return number_string


def DictToValueTuple(d, key_tuple, format_tuple):
  """Converts a dict of values to send via serial to a tuple values in correct sequence.

  Many values must ultimately be sent to struct.pack in exactly the right sequence, but
  it's difficult to work with and debug a large list of values where the context depends

  on its position. Instead, we can use a dict, where the keys are the strings in the key
  tuple. A value tuple is formed comprised of the values of the input dictionary in the
  same sequence as the keys.

  Additionally, any values that are defined as strings as specified by the corresponding
  element in the format_tuple are truncated if necessary.

  Args:
    d: dictionary to transform into a value tuple.
    key_tuple: tuple of keys that matches d.keys().
    format_tuple: tuple of format specifiers (consistent with struct.pack formats) for
      the key_tuple.

  Raises:
    ValueError: Raised if the key_tuple elements and d.keys() do not match exactly.


  Returns:
    Tuple of values of same length as key_tuple and format_tuple; additionally strings
    are truncated if necessary and converted from ascii to bytes.

  """
  if set(key_tuple) != set(d.keys()):

    raise ValueError('Non matching sets of keys in d (%s) and key_tuple (%s)' % (
        sorted(list(d.keys())), sorted(list(key_tuple))))

  values = []
  for n, k in enumerate(key_tuple):
    if format_tuple[n].endswith('s'):
      value = d[k]
      # 9s, for example, means 9 bytes will be sent; since a string must end with a
      # terminating character, the max length string that can be fit into a 9s field
      # is 8 characters.
      max_length = int(format_tuple[n][:-1]) - 1
      if len(value) > max_length:
        Log('At most an %d-length string is expected, yet d[%s] = %s, %d characters;'

            ' string truncated to fit' % (max_length, k, value, len(value)))
      truncated_string = d[k][:max_length]
      string_bytes = bytes(truncated_string, 'ascii')
      values.append(string_bytes)
    else:
      values.append(d[k])

  return tuple(values)


def GetNow(json_desc_dict, additional_attr):
  """Identifies the epoch to use in the Arduinos for the "current" time, i.e.: now.

  Simulations should use a timestamp contemporaneous with the flights, whereas live
  data should use the current timestamp.
  """
  if not additional_attr:
    return 0
  if additional_attr['simulation']:
    return json_desc_dict['now']
  return time.time()


def GenerateRemoteMessage(
    flight, json_desc_dict, configuration, additional_attr, display_mode):
  """Generates a value-tuple to be packed and sent to the arduino remote.

  Args:
    flight: dictionary describing the most recent flight.
    json_desc_dict: dictionary representing the current radio signal.
    configuration: dictionary representing the current state of the messageboard
      configuration.
    additional_attr: dictionary with miscellaneous attributes from messageboard.
    display_mode: integer specifying the display mode, so that the text display lines
      can be appropriately configured.

  Returns:
    Dictionary of values, where the dict keys and types are specified by
    RemoteMain.write_config.
  """
  flight_last_seen = flight.get('now')  # time flight was seen

  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:




                            <----SKIPPED LINES---->




  d['line1_dec_mask'] = int(line1_decimal_mask, 2)
  d['line2_dec_mask'] = int(line2_decimal_mask, 2)
  d['display_mode'] = display_mode

  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
  log_lines = []
  # makes a copy so as to not modify underlying config; we don't want to modify underlying
  # because otherwise the settings will bounce around (values read from disk ->
  # -> new values set by arduino -> old values from disk -> new values from disk after
  # Arduino update).
  configuration = dict(configuration)
  setting_keys = ['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.get(key)), str(command[key])))
      setting_change = True
    configuration[key] = command[key]
  remote_key = 'setting_screen_enabled_bool'
  system_key = 'setting_screen_enabled'
  # remote sees T/F whereas messageboard.py & the web interface expect 'on'/absent key

  if command[remote_key] and system_key not in configuration:
    setting_change = True
    configuration[system_key] = 'on'
    log_lines.append(' |-->Setting %s updated from None to on' % system_key)
  elif not command[remote_key] and system_key in configuration:
    setting_change = True
    configuration.pop(system_key)
    log_lines.append(' |-->Setting %s updated from on to None' % system_key)

  if setting_change:
    settings_string = messageboard.BuildSettings(configuration)
    to_parent_q.put(('update_configuration', (settings_string, )))

  # a command might request info about flight to be (re)displayed, irrespective of
  # whether the screen is on; if so, let's put that message at the front of the message

  # queue, and delete any subsequent messages in queue because presumably the button
  # was pushed either a) when the screen was off (so no messages in queue), or b)
  # because the screen was on, but the last flight details got lost after other screens!

  if command['last_plane']:
    to_parent_q.put(('replay', ()))
    log_lines.append(' |-->Requested last flight (re)display')

  # a command might request a histogram; simply generate and save a histogram file to disk
  if command['histogram_enabled']:
    h_type = GetName(HISTOGRAM_TYPES, command['current_hist_type'])
    h_history = GetName(HISTOGRAM_HISTORY, command['current_hist_history'])
    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]))
    messageboard.WriteFile(REMOTE_DISPLAY_MODE, str(command['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 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 round(s, 1) < 10:
    numeric_string = '%sS' % FloatToAlphanumericStr(s, 1, 3, sign=False)
    partial_decimal_mask = decimal_after_first_character
  elif s < messageboard.SECONDS_IN_MINUTE:




                            <----SKIPPED LINES---->




    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 round(h, 1) < 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 round(d, 1) < 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.

    randomized: Boolean indicating whether the command generation is deterministic or
      randomized.

  Returns:
    Potentially empty string representing a fully formed command that might come from
    an remote, had one been connected.
  """
  # should we generate a command?
  generate = False
  if randomized:
    generate = random.random() < fraction_command
  else:
    generate = int(fraction_command * counter) != int(fraction_command * (counter - 1))

  if not generate:
    return ''

  # which command should we pick?
  if randomized:
    index = random.randrange(len(potential_commands))
  else:
    command_number = int(counter * fraction_command) - 1
    index = command_number % len(potential_commands)
  return potential_commands[index]


def ReplaceWithNA(s, function=None):
  """Replaces a messageboard not-known value with the alphanumeric not-known value."""
  if s in (messageboard.KEY_NOT_PRESENT_STRING, 'None') or s is None:
    return KEY_NOT_PRESENT_STRING
  if function:
    return function(s)
  return s


def GetId(mapping, name):
  """From a mapping tuple of 2-tuples of form (number, name), converts name into number."""
  for t in mapping:
    if t[1] == name:
      return t[0]
  raise ValueError('%s is not in %s' % (name, mapping))


def GetName(mapping, n):
  """From a mapping tuple of 2-tuples of form (number, name), converts number into name."""
  for t in mapping:
    if t[0] == n:
      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
      ('display_mode',                'H'),  # 2 bytes
      ('last_flight_available',       '?'),  # 1 byte
  )
  #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=60,
      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()

  # Read in the saved display mode, if it exists
  display_mode = messageboard.ReadFile(REMOTE_DISPLAY_MODE, log_exception=False)
  if not display_mode:
    display_mode = DISP_LAST_FLIGHT_NUMB_ORIG_DEST
  else:
    display_mode = int(display_mode)

  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)





                            <----SKIPPED LINES---->





01234567890123456789012345678901234567890123456789012345678901234567890123456789
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 5556  57585960616263646566676869707172737475767778798081828384858687








96979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507








527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747








773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939








971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101








110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341








#!/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, SHUTDOWN_TEXT)
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

REMOTE_DISPLAY_MODE = 'display_mode.txt'

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
  REMOTE_DISPLAY_MODE = MESSAGEBOARD_PATH + REMOTE_DISPLAY_MODE

  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))

LASER_OFF = (False, False, False)
LASER_ALL = (True, True, True)
LASER_RED = (True, False, False)
LASER_GREEN = (False, True, False)
LASER_BLUE = (False, False, True)

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'

DISP_LAST_FLIGHT_NUMB_ORIG_DEST = 0
DISP_LAST_FLIGHT_AZIMUTH_ELEVATION = 1
DISP_FLIGHT_COUNT_LAST_SEEN = 2
DISP_RADIO_RANGE = 3
DISPLAY_MODE_NAMES = [
    'LAST_FLIGHT_NUMB_ORIG_DEST', 'LAST_FLIGHT_AZIMUTH_ELEVATION',
    'FLIGHT_COUNT_LAST_SEEN', 'RADIO_RANGE']

WRITE_DELAY_TIME = 0.2  # write to arduino every n seconds
READ_DELAY_TIME = 0.1  # read from arduino every n seconds

HISTOGRAM_TYPES = (
    (0, 'hour'),
    (1, 'day_of_month'),
    (2, 'origin'),




                            <----SKIPPED LINES---->





HISTOGRAM_HISTORY = (
    (0, 'today'),
    (1, '24h'),
    (2, '7d'),
    (3, '30d'))


def Log(message, ser=None, file=ARDUINO_LOG):
  """Logs with additional serial-connection details if provided"""
  if ser:
    additional_text = str(ser)
  else:
    additional_text = 'ARDUINO'

  if file == ARDUINO_LOG:
    rolling = ARDUINO_ROLLING_LOG
  else:
    rolling = None

  messageboard.Log(
      '%s: %s' % (additional_text, message), file=file, rolling=rolling)


class Serial():
  """Serial object that allows for connection types to be easily swapped.

  The Serial class allows for a persistent connection over one of
  several types of serial connections: USB, bluetooth, and simulation
  via text file.  It fully encapsulates error management so that the
  details of reconnecting on errors are hidden, yet it does check for and
  recover from timeouts, dropped connections, etc.

  Note that to read to/from Arduino, the data sizes (in bytes)
  expected by struct must match that in C. However, there are some
  inconsistencies amongst identically-named types - for instance, a
  Python int is 4 bytes, whereas a C int is 2 bytes.

  The following mappings on types to use with a format string and C declarations
  is valid:
  - C bool (1 byte) to struct ?
  - C short (2 bytes) to struct h
  - C long (4 bytes) to struct l
  - C float (4 bytes) to struct f
  - C char[] (1 byte per character) to struct s
  Struct formats: https://docs.python.org/3/library/struct.html
  Arduino types: https://robotresearchlab.com/2016/11/14/variable-data-types/
  """
  def __init__(
      self, connection_type, connection_tuple, baud=9600, open_timeout=5,
      read_format=None, write_format=None, read_timeout=0, error_pin=None,
      to_parent_q=None, name=None):
    """Creates a new instance of the Serial class.

    Args:
      connection_type: This identifies whether we are connecting
        with bluetooth, usb, or via a simulated connection; based
        on this, we identify an opening method, which is then called
         as open_function(connection_tuple, baud=baud, timeout=open_timeout)
      connection_tuple: A tuple of connection details specific to how
        the serial object connects - i.e.: a mac address, or a port name, etc.
      baud: Baud rate for the connection.
      open_timeout: The number of seconds we should wait on opening
        a new connection before timing out.
      read_format: Since reads from a serial device are often using
        the same struct.pack format_string for all calls, this allows
        that to be instantiated once and then referenced as needed.
      write_format: Similar concept to read_format, but for sending
        messages to the serial device.
      read_timeout: If the serial device is providing a heartbeat, this
        accelerates the identification of a dropped connection by trying
        to reconnect as soon as the time difference between the last
        non-empty read, and the last read attempt, exceeds this many
        seconds. Note, however, that we may not necessarily be able to
        recover more quickly, based on timeouts in the underlying libraries
        and systems; this can be disabled by setting it to 0.
      error_pin: If set, the messageboard GPIO pin is set to high
        whenever we have a failed / non-open connection, and low whenever
        we believe we have reconnected.
      to_parent_q: message queue to send status updates to parent
      name: an optional text string used in some logging to help identify which
        serial connection the log message is associated with.
    """
    self.connection_type = connection_type
    self.connection_tuple = connection_tuple
    self.baud = baud
    self.open_timeout = open_timeout
    self.read_format = read_format
    self.write_format = write_format
    self.read_timeout = read_timeout
    self.error_pin = error_pin
    self.to_parent_q = to_parent_q
    self.last_read = 0
    self.last_receipt = 0
    self.reset_flag = True
    self.link = None
    self.name = name

    self.__simulated_reads__ = None

    if self.connection_type == CONNECTION_FLAG_BLUETOOTH:
      self.open_function = OpenBluetooth
    elif self.connection_type == CONNECTION_FLAG_USB:
      self.open_function = OpenUSB
    elif self.connection_type == CONNECTION_FLAG_SIMULATED:
      self.open_function = None

    if self.error_pin:  # Error on when main initiated; off when connected
      error_message = 'Process %s (%s) initialized into error state' % (
          os.getpid(), str(self.name))
      Log(error_message)
      self.to_parent_q.put(('pin', (self.error_pin, True, error_message)))

    self.start_time = time.time()

  def __str__(self):
    return self.name

  def Open(self):
    """Opens an instantiated serial connection for reading and writing."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      lines = []
      if os.path.exists(self.connection_tuple[0]):
        with open(self.connection_tuple[0], 'r') as f:
          for line in f:
            if line.strip():
              lines.append(eval(line))  # pylint: disable=W0123
      else:
        Log('File %s does not exist for simulated commands to Arudino'
            % self.connection_tuple[0], self.link)
      self.__simulated_reads__ = lines

      # clear out file so that shell tail -f process can
      # continue to point to same file
      with open(self.connection_tuple[1], 'w') as f:
        f.write('')

      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, False)))
      return

    self.link = self.open_function(
        self.connection_tuple, name=self.name,
        baud=self.baud, timeout=self.open_timeout)
    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, False)))
    self.last_read = time.time()
    self.last_receipt = time.time()
    self.reset_flag = True

  def Reopen(self, log_message=None):
    """Closes and reopens a link, optionally logging a message."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')

    self.link = ReopenConnection(
        self.open_function, self.link, self.connection_tuple,
        name=self.name,
        baud=self.baud, timeout=self.open_timeout, log_message=log_message)
    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, False)))
    self.reset_flag = True
    self.last_read = time.time()
    self.last_receipt = time.time()

  def Close(self, close_message):
    """Closes an open serial connection."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      return

    self.link.close()
    if self.error_pin:
      self.to_parent_q.put(('pin', (self.error_pin, True, close_message)))
    Log(close_message, self)

  def Available(self):
    """Calls self.link.available()."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')
    self.link.available()

  def Read(self, format_string=None, bytes_read=None):
    """Reads from an open serial.

    Reads from 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, or if this read, in
    failing to return non-empty results, means that the heartbeat
    timeout time has elapsed, this method will attempt to reopen the connection.


    Args:
      format_string: String of the form expected by struct.pack
      bytes_read: if passed, the bytes that are read are appended to the list.

    Returns:
      Tuple of values matching that as identified in format_string.
    """
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      if (
          self.__simulated_reads__ and
          # time for next
          time.time() - self.start_time > self.__simulated_reads__[0][0]):
        next_line = self.__simulated_reads__.pop(0)
        return next_line[1]
      return ()

    if not format_string:
      format_string = self.read_format
    try:
      data = Read(self.link, format_string, bytes_read=bytes_read)
    except OSError as e:
      failure_message = 'Failed to read from %s: %s' % (self.name, e)
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      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):
      failure_message = (
          'Heartbeat not received in %.2f seconds (expected: %.2f) on %s' % (
              self.last_read - self.last_receipt, self.read_timeout, self.name))
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      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))
      return

    if not format_string:
      format_string = self.write_format
    try:
      Write(self.link, values, format_string)
    except OSError as e:
      failure_message = 'Failed to write: %s' % e
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True, failure_message)))
      self.Reopen(log_message=failure_message)
      self.Write(values)
    if LOG_SERIALS:
      with open(SERIALS_LOG, 'a') as f:
        f.write('%10.3f SENT@%s: %s\n' % (ts, self.name, str_values))

  def HasReset(self):
    """Indicates exactly once whether serial has reset since last called."""
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      raise NotImplementedError('Not implemented for simulations')

    flag = self.reset_flag
    self.reset_flag = False
    return flag


def RunCommand(cmd, sleep_seconds=1, log=True):
  """Runs shell command, checking if it completed within timeout."""
  conn = subprocess.Popen(cmd, shell=True)
  time.sleep(sleep_seconds)
  conn.poll()

  if conn.returncode is None:
    Log('ERROR: %s did not complete within %d seconds'
        % (cmd, sleep_seconds))
    sys.exit()

  if log:
    Log('%s completed' % cmd)


def OpenBluetooth(connection_tuple, baud=9600, timeout=5, name=None):
  """Attempts to open bluetooth, exiting program on fail.

  This may fail due to a missing /dev/rfcomm# entry and inability to
  create new one (or an existing one bound to same device number but
  pointing to a different serial connection), expectation of a handshake
  but lack of receipt of one, or timeouts in general before receipt.

  Args:
    connection_tuple: A 3-tuple of the rfcomm_device number (i.e.:
      the 1 in /dev/rfcomm1), mac_address of the bluetooth radio, and
      the bluetooth channel; all must be provided.
    baud: speed of the connection.
    timeout: seconds after a connection is established that this polls
      for a heartbeat before failing; a value of zero indicates no
      handshake needed.
    name: string name of the connection to display in error logging.

  Returns:
    An open pySerialTransfer.SerialTransfer link.
  """
  rfcomm_device, bt_mac_address, channel = connection_tuple

  attempts = 5  # max number of attempts to make a connection
  attempt = 1
  link = None
  dev = '/dev/rfcomm%d' % rfcomm_device
  if not name:
    name = '%s @ %s' % (bt_mac_address, dev)

  if not os.path.exists(dev):
    RunCommand(
        'sudo rfcomm bind %d %s %d' % (rfcomm_device, bt_mac_address, channel))

  while attempt <= attempts:
    link = pySerialTransfer.SerialTransfer(dev, baud)

    # We think we made a connection; lets see if we can get a receipt
    start = time.time()
    if not timeout:
      return link
    try:
      while time.time() - start < timeout:
        b = ReadBytes(link)
        time.sleep(0.1)  # avoid a tight loop
        if b:
          Log('Handshake received at %s by receipt of bytes %s' % (name, b))
          return link
      Log('No handshake received at %s after %d seconds on attempt %d'
          % (name, timeout, attempt))
    except OSError as e:
      Log('Handshake error with %s on attempt %d: %s' % (name, attempt, e))
    attempt += 1
  Log('ERROR: Failed to connect to %s after %d attempts' % (
      bt_mac_address, attempt - 1))
  sys.exit()


def OpenUSB(connection_tuple=('arduino', None), baud=9600, timeout=5):
  """Attempts to open USB for a number of seconds, exiting program on fail.

  This may fail due to lack of a plugged-in serial device matching the
  given definition.

  Args:
    connection_tuple: A 2-tuple of the manufacturer and the serial number
      of the expected device to connect with; either one may be missing
      (by providing a None value).
    baud: speed of the connection.
    timeout: seconds polling for the device matching the connection_tuple
      before fail on timeout.

  Raises:
    serial.SerialException: raised if no serial matching given attributes found

  Returns:
    An open pySerialTransfer.SerialTransfer link.
  """
  manufacturer, sn = connection_tuple
  initial_time = time.time()
  arduino_port = None
  attempted = False
  while (
      not attempted or time.time() - initial_time < timeout
      and not arduino_port):
    attempted = True
    ports = serial.tools.list_ports.comports(include_links=False)
    for port in ports:

      port_mfg = port.manufacturer
      if port_mfg:
        port_mfg = port_mfg.lower()
      match_mfg = not manufacturer or (
          port_mfg and manufacturer.lower() in port_mfg)

      port_sn = port.serial_number
      match_sn = not sn or port_sn == sn

      if match_mfg and match_sn:
        arduino_port = port.device
        break

  link = None
  if arduino_port:
    link = pySerialTransfer.SerialTransfer(arduino_port, baud=baud)
    time.sleep(2)  # Need to give Arduino time before it is ready to receive
  else: # no USB-based matching port found
    raise serial.SerialException(
        'ERROR: No USB port found for mfg %s and sn %s' % (manufacturer, sn))

  return link


def ReopenConnection(




                            <----SKIPPED LINES---->




  """Reads the bytes at the link, returning a byte object."""
  read_bytes = []
  try:
    if link.available():
      if link.status < 0:
        raise serial.SerialException('ERROR: %s' % link.status)

      read_bytes = []
      for index in range(link.bytesRead):
        read_bytes.append(link.rxBuff[index])
  except (serial.SerialException, termios.error) as e:
    Log('Error in ReadBytes: %s' % e)
    read_bytes = []
  return read_bytes


def Unpack(read_bytes, format_string):
  """Unpacks a byte object into a tuple of values."""
  try:
    data = struct.unpack(format_string, bytearray(read_bytes))
  except struct.error as e:  # exception is missing some key detail, so re-raise
    raise struct.error(
        '%s but %d bytes provided (%s)' % (e, len(read_bytes), read_bytes))
  data = [
      str(d, 'ascii').split('\x00')[0]
      if isinstance(d, bytes) else d for d in data]
  return data


def Read(link, format_string, bytes_read=None):
  """Read and unpacks the bytes in one go."""
  read_bytes = ReadBytes(link)
  if isinstance(bytes_read, list):
    bytes_read.extend(read_bytes)
  data = ()
  if read_bytes:
    data = Unpack(read_bytes, format_string)
  return data


def AzimuthAltitude(flight, now):
  """Provides current best-estimate location details given last known position.

  Given a flight dictionary, this determines the plane's best estimate
  for current location using its last-known position in the flight's lat /
  lon / speed / altitude / and track. Those attributes may have already
  been updated by messageboard using a more recently obtained radio signal
  from dump1090 than that in the canonical location, and if so, key
  flight_loc_now indicates the time at which those locations are current
  as of.

  Args:
    flight: dictionary of flight attributes.
    now: epoch indicating the timestamp for which the azimuth and
      altitude should be calculated.

  Returns:
    Returns a tuple of the azimuth and altitude, in degrees, at the
    current system time.
  """
  if not flight:
    return None
  persistent_path = flight.get('persistent_path')
  if not persistent_path:
    return None

  most_recent_loc = persistent_path[-1]
  lat = most_recent_loc.get('lat')
  lon = most_recent_loc.get('lon')
  speed = most_recent_loc.get('speed')
  altitude = most_recent_loc.get('altitude')
  track = most_recent_loc.get('track')
  loc_now = most_recent_loc.get('now')
  unused_vert_rate = most_recent_loc.get('vert_rate')

  if not all([isinstance(x, numbers.Number) for x in (
      lat, lon, speed, altitude, track)]):
    return None

  elapsed_time = now - loc_now
  meters_traveled = messageboard.MetersTraveled(speed, elapsed_time)
  new_position = messageboard.TrajectoryLatLon(
      (lat, lon), meters_traveled, track)
  unused_distance = messageboard.HaversineDistanceMeters(
      new_position, messageboard.HOME)

  # debugging messages to help identify whether servos
  # are getting correct flight path info
  if elapsed_time < 60 and VERBOSE:
    Log('flight %s\n'
        'most_recent_loc: %s\n'
        'persistent path data: lat: %.5f; lon: %.5f; '
        'speed: %d; track: %d; altitude: %d\n'
        'now: %.5f\n'
        'loc_now: %.5f\n'
        'since measured data: elapsed_time: %.2f; '
        'meters_traveled: %.2f; new_position: %.5f, %.5f' % (
            messageboard.DisplayFlightNumber(flight),
            str(most_recent_loc),
            lat, lon, speed, track, altitude,
            now,
            loc_now,
            elapsed_time, meters_traveled, *new_position))

  angles = messageboard.Angles(
      messageboard.HOME,
      messageboard.HOME_ALT,
      new_position,
      altitude / messageboard.FEET_IN_METER)
  azimuth = angles['azimuth_degrees']
  altitude = angles['altitude_degrees']
  return (azimuth, altitude)

def DrainQueue(q):
  """Empties a queue, returning the last-retrieved value."""
  value = None
  while not q.empty():
    value = q.get(block=False)
  return value


def InitialMessageValues(q):
  """Initializes the arduino main processes with values from messageboard."""
  v = DrainQueue(q)
  if v:
    return v
  return {}, {}, {}, {}


def ServoTestOrdinal(link):
  """Point laser at each of 0, 90, 180, 270 and hold with different colors."""
  link.Write((0, 0, *LASER_ALL))
  time.sleep(1)
  link.Write((90, 0, *LASER_RED))
  time.sleep(1)
  link.Write((180, 0, *LASER_GREEN))
  time.sleep(1)
  link.Write((270, 0, *LASER_BLUE))
  time.sleep(1)


def ServoTestSweep(link, altitude=45):
  """Sweep red laser around 360 degrees."""
  for azimuth in range(0, 360, 10):
    link.Write((azimuth, altitude, *LASER_RED))
    time.sleep(WRITE_DELAY_TIME)


def ServoMain(to_arduino_q, to_parent_q, shutdown):
  """Main servo controller for projecting the plane position on a hemisphere.

  Takes the latest flight from the to_arduino_q and converts that to the current
  azimuth and altitude of the plane on a hemisphere.
  """
  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()

  # write_format: azimuth, altitude, R, G, & B intensity
  # read heartbeat: millis
  link = Serial(
      *SERVO_CONNECTION, read_timeout=60,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION,
      to_parent_q=to_parent_q,
      read_format='l', write_format='ff???', 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)

  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_ordinal' in configuration:
        messageboard.RemoveSetting(configuration, 'test_servos_ordinal')
        ServoTestOrdinal(link)
      elif 'test_servos_sweep' in configuration:
        messageboard.RemoveSetting(configuration, 'test_servos_sweep')
        ServoTestSweep(link)

      new_flight = DifferentFlights(flight, last_flight)
      if new_flight:
        Log('Flight changed from %s to %s' % (
            messageboard.DisplayFlightNumber(last_flight),
            messageboard.DisplayFlightNumber(flight)
        ), ser=link)

        # Turn off laser so line isn't traced while it moves to new position
        link.Write((*last_angles, *LASER_OFF))

      last_flight = flight

    if time.time() >= next_read:
      heartbeat = link.Read()  # simple ack message sent by servos
      next_read = time.time() + READ_DELAY_TIME
      if heartbeat and VERBOSE:
        Log(heartbeat)

    now = GetNow(json_desc_dict, additional_attr)

    current_angles = AzimuthAltitude(flight, now)
    if current_angles and time.time() > next_write:
      if current_angles[1] >= configuration['minimum_altitude_servo_tracking']:
        if VERBOSE:
          Log('Flight #: %s current_angles: %s' % (
              messageboard.DisplayFlightNumber(flight), str(current_angles)))
        laser_rgb = LaserRGBFlight(flight)
        link.Write((*current_angles, *laser_rgb))




                            <----SKIPPED LINES---->




  return LASER_BLUE


def DifferentFlights(f1, f2):
  """True if both squawk and flight number different; false otherwise."""
  if f1 is None and f2 is None:
    return True
  if f1 is None or f2 is None:
    return True

  if (
      f1.get('flight_number') != f2.get('flight_number')
      and f1.get('squawk') != f2.get('squawk')):
    return True
  return False


def FloatToAlphanumericStr(x, decimals, total_length, sign=True):
  """Formats a float as a string without a decimal point.

  Since the decimal point is controlled independently on the
  alphanumeric display, numbers that include decimals must be sent
  without the decimal point.  This formats a float as %+5.2f, for
  example - but without the decimal.

  Args:
    x: Value to format.
    decimals: how many digits should follow the (absent) decimal point.
    total_length: desired total length of the resulting string-ified
      number (inclusive of the absent decimal point.
    sign: boolean indicating whether a sign should be included for
      positive numbers.

  Returns:
    String as specified - for instance, 0.4 might be formatted as ' +04'.
  """
  sign_str = ''
  if sign:
    sign_str = '+'
  format_string = '%' + sign_str + str(total_length) + '.' + str(decimals) + 'f'
  number_string = format_string % x
  number_string = number_string.replace('.', '')
  return number_string


def DictToValueTuple(d, key_tuple, format_tuple):
  """Converts dict of values to tuple values in correct sequence.

  Many values must ultimately be sent to struct.pack in exactly the
  right sequence, but it's difficult to work with and debug a large
  list of values where the context depends on its position. Instead,
  we can use a dict, where the keys are the strings in the key tuple.
  A value tuple is formed comprised of the values of the input
  dictionary in the same sequence as the keys.

  Additionally, any values that are defined as strings as specified
  by the corresponding element in the format_tuple are truncated if necessary.

  Args:
    d: dictionary to transform into a value tuple.
    key_tuple: tuple of keys that matches d.keys().
    format_tuple: tuple of format specifiers (consistent with struct.pack
      formats) for the key_tuple.

  Raises:
    ValueError: Raised if the key_tuple elements and d.keys() do not
    match exactly.

  Returns:
    Tuple of values of same length as key_tuple and format_tuple;
    additionally strings are truncated if necessary and converted from
    ascii to bytes.
  """
  if set(key_tuple) != set(d.keys()):
    raise ValueError(
        'Non matching sets of keys in d (%s) and key_tuple (%s)' %
        (sorted(list(d.keys())), sorted(list(key_tuple))))

  values = []
  for n, k in enumerate(key_tuple):
    if format_tuple[n].endswith('s'):
      value = d[k]
      # 9s, for example, means 9 bytes will be sent; since a string must
      # end with a terminating character, the max length string that can
      # be fit into a 9s field is 8 characters.
      max_length = int(format_tuple[n][:-1]) - 1
      if len(value) > max_length:
        Log('At most an %d-length string is expected, yet d[%s] = %s, %d'
            ' characters; string truncated to fit' %
            (max_length, k, value, len(value)))
      truncated_string = d[k][:max_length]
      string_bytes = bytes(truncated_string, 'ascii')
      values.append(string_bytes)
    else:
      values.append(d[k])

  return tuple(values)


def GetNow(json_desc_dict, additional_attr):
  """Identifies epoch to use in the Arduinos for the "current" time, i.e.: now.

  Simulations should use a timestamp contemporaneous with the flights,
  whereas live data should use the current timestamp.
  """
  if not additional_attr:
    return 0
  if additional_attr['simulation']:
    return json_desc_dict['now']
  return time.time()


def GenerateRemoteMessage(
    flight, json_desc_dict, configuration, additional_attr, display_mode):
  """Generates a value-tuple to be packed and sent to the arduino remote.

  Args:
    flight: dictionary describing the most recent flight.
    json_desc_dict: dictionary representing the current radio signal.
    configuration: dictionary representing the current state of
      the messageboard configuration.
    additional_attr: dictionary with miscellaneous attributes from messageboard.
    display_mode: integer specifying the display mode, so that the text
      display lines can be appropriately configured.

  Returns:
    Dictionary of values, where the dict keys and types are specified by
    RemoteMain.write_config.
  """
  flight_last_seen = flight.get('now')  # time flight was seen

  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:




                            <----SKIPPED LINES---->




  d['line1_dec_mask'] = int(line1_decimal_mask, 2)
  d['line2_dec_mask'] = int(line2_decimal_mask, 2)
  d['display_mode'] = display_mode

  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 write to disk
  setting_change = False
  log_lines = []
  # makes a copy so as to not modify underlying config; we don't want
  # to modify underlying because otherwise the settings will bounce
  # around (values read from disk -> new values set by arduino -> old
  # values from disk -> new values from disk after Arduino update).
  configuration = dict(configuration)
  setting_keys = ['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.get(key)), str(command[key])))
      setting_change = True
    configuration[key] = command[key]
  remote_key = 'setting_screen_enabled_bool'
  system_key = 'setting_screen_enabled'
  # remote sees T/F whereas messageboard.py & the web interface expect
  # 'on'/absent key
  if command[remote_key] and system_key not in configuration:
    setting_change = True
    configuration[system_key] = 'on'
    log_lines.append(' |-->Setting %s updated from None to on' % system_key)
  elif not command[remote_key] and system_key in configuration:
    setting_change = True
    configuration.pop(system_key)
    log_lines.append(' |-->Setting %s updated from on to None' % system_key)

  if setting_change:
    settings_string = messageboard.BuildSettings(configuration)
    to_parent_q.put(('update_configuration', (settings_string, )))

  # a command might request info about flight to be (re)displayed,
  # irrespective of whether the screen is on; if so, let's put that
  # message at the front of the message queue, and delete any
  # subsequent messages in queue because presumably the button was
  # pushed either a) when the screen was off (so no messages in queue),
  # or b) because the screen was on, but the last flight details got
  # lost after other screens!
  if command['last_plane']:
    to_parent_q.put(('replay', ()))
    log_lines.append(' |-->Requested last flight (re)display')

  # command might request a histogram
  if command['histogram_enabled']:
    h_type = GetName(HISTOGRAM_TYPES, command['current_hist_type'])
    h_history = GetName(HISTOGRAM_HISTORY, command['current_hist_history'])
    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]))
    messageboard.WriteFile(REMOTE_DISPLAY_MODE, str(command['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 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 3-char 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 round(s, 1) < 10:
    numeric_string = '%sS' % FloatToAlphanumericStr(s, 1, 3, sign=False)
    partial_decimal_mask = decimal_after_first_character
  elif s < messageboard.SECONDS_IN_MINUTE:




                            <----SKIPPED LINES---->




    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 round(h, 1) < 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 round(d, 1) < 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.
    randomized: Boolean indicating whether the command generation is
      deterministic or randomized.

  Returns:
    Potentially empty string representing a fully formed command that might
    come from an remote, had one been connected.
  """
  # should we generate a command?
  generate = False
  if randomized:
    generate = random.random() < fraction_command
  else:
    generate = int(fraction_command * counter) != int(
        fraction_command * (counter - 1))
  if not generate:
    return ''

  # which command should we pick?
  if randomized:
    index = random.randrange(len(potential_commands))
  else:
    command_number = int(counter * fraction_command) - 1
    index = command_number % len(potential_commands)
  return potential_commands[index]


def ReplaceWithNA(s, function=None):
  """Replaces a messageboard not-known with alphanumeric not-known value."""
  if s in (messageboard.KEY_NOT_PRESENT_STRING, 'None') or s is None:
    return KEY_NOT_PRESENT_STRING
  if function:
    return function(s)
  return s


def GetId(mapping, name):
  """From a mapping tuple of 2-tuples of (#, name), converts name into #."""
  for t in mapping:
    if t[1] == name:
      return t[0]
  raise ValueError('%s is not in %s' % (name, mapping))


def GetName(mapping, n):
  """From a mapping tuple of 2-tuples of (#, name), converts # into name."""
  for t in mapping:
    if t[0] == n:
      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 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 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 & terminator
      ('line2',                       '9s'), # 9 bytes; 8 character & terminator
      ('line1_dec_mask',              'H'),  # 2 bytes
      ('line2_dec_mask',              'H'),  # 2 bytes
      ('display_mode',                'H'),  # 2 bytes
      ('last_flight_available',       '?'),  # 1 byte
  )
  #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=60,
      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()

  # Read in the saved display mode, if it exists
  display_mode = messageboard.ReadFile(REMOTE_DISPLAY_MODE, log_exception=False)
  if not display_mode:
    display_mode = DISP_LAST_FLIGHT_NUMB_ORIG_DEST
  else:
    display_mode = int(display_mode)

  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)





                            <----SKIPPED LINES---->