arduino-2020-05-14-1343.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789
123456789101112131415161718192021222324  2526272829   30        3132    33 34  3536373839404142  434445464748495051525354555657            585960               616263                    64656667                             68697071727374 7576 77           7879       8081                     828384858687888990    9192939495 96979899    100  101102   103104105                      106107108109110111112113114115116117     118119120121122123124125126127                 128129130131132133134135136            137138139 140141142143144145146147148149150151152153154                155156157158159160161162163164165166167168169170  171172173174175176177178179180181182183184185186187188  189190191192      193194195196197198199200201202203204205206207208209210211212213214215








219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 266267268269270271272273274275276277 278279280281282283284285286287 288289290291292293294295296297298299300301302303304305306  307308309310311312313314      315316 317  318319  320321322323               324325326327328329330331332333334335336  337338339340341342 343344345346347348349     350        351352353 354355356357358359360361362363364   365366367368           369 370  371372373374     375376377378379380381382383   384385386387388389390391392393394395396  397398399400401402403404     405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457       458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490      491492                                                    493494495496497498499 500501502503504505506507508509510511512513 514515516517518519520521   522523524525526   527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557          558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637   638639640 641642643644645646647648649650651652653654655656657658659660661662663  664 665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706      707708709  710711712713714715716717718719720721722723724725726727728729730731








774775776777778779780781782783784785786787788789790791792793                       794      795796797798799800801802803804805806807808                            809810811812813 814815816817818819820821822823824825826  827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875                 876   
#!/usr/bin/python3

import contextlib
import io
import os
import numbers
import queue
import random
import struct
import subprocess
import sys
import time


import psutil
import serial
import serial.tools.list_ports
from pySerialTransfer import pySerialTransfer

import messageboard


VERBOSE = True



SIMULATE_ARDUINO = False
if '-s' in sys.argv:
  SIMULATE_ARDUINO = True

SIMULATION_FILE = 'remote_simulation.txt'  #was arduino_simulation.txt













SN_SERVO = '75835343130351802272'  # arduino uno serial




SN_REMOTE = '5583834303435111C1A0'  # arduino mega serial





SETTINGS_READ_TIME = 1  # read settings file once every n seconds
LATEST_FLIGHT_READ_TIME = 1  # read flight file file once every n seconds
COMMAND_DELAY_TIME = 1  # send a command to servos every n seconds

# Serial interaction with remote
COMMAND_START_CHAR = '<'
COMMAND_END_CHAR = '>'


KEY_NOT_PRESENT_STRING = 'N/A'

MAX_CMD_LENGTH = 250
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']

SETTINGS_READ_TIME = 1  # read settings file once every n seconds
LATEST_FLIGHT_READ_TIME = 1  # read flight file file once every n seconds
COMMAND_DELAY_TIME = 0.2  # send a command to remote every n seconds














def Log(s):
  print('ARDUINO: ' + s)  # TODO: change back to logging

















class Serial():




















  def __init__(
      self, open_function, connection_tuple, baud=9600, open_timeout=5,
      read_format=None, write_format=None, read_timeout=0, error_pin=None):
    self.open_function = open_function





























    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.last_read = 0
    self.last_receipt = 0

    self.link = None











    if self.error_pin:
      messageboard.UpdateStatusLight(self.error_pin, True)








  def Open(self):





















    self.link = self.open_function(
        self.connection_tuple, baud=self.baud, timeout=self.open_timeout)
    if self.error_pin:
      messageboard.UpdateStatusLight(self.error_pin, False)
    self.last_read = time.time()
    self.last_receipt = time.time()
    return self.link

  def Reopen(self, log_message=None):




    self.link = ReopenConnection(
        self.open_function, self.link, self.connection_tuple,
        baud=self.baud, timeout=self.open_timeout, log_message=log_message)
    if self.error_pin:
      messageboard.UpdateStatusLight(self.error_pin, False)

    self.last_read = time.time()
    self.last_receipt = time.time()

  def Close(self):




    self.link.close()



  def Available(self):



    self.link.available()

  def Read(self, format_string=None):






















    if not format_string:
      format_string = self.read_format
    try:
      data = Read(self.link, format_string)
    except OSError as e:
      if self.error_pin:
        messageboard.UpdateStatusLight(self.error_pin, True)
      self.Reopen(log_message='Failed to read: %s' % e)
      return self.Read(format_string=format_string)
    self.last_read = time.time()
    if data:
      self.last_receipt = time.time()





    if self.read_timeout and self.last_read - self.last_receipt > self.read_timeout:
      if self.error_pin:
        messageboard.UpdateStatusLight(self.error_pin, True)
      self.Reopen(log_message='Heartbeat not received in %.2f seconds'
                  % (self.last_read - self.last_receipt))
      return self.Read(format_string=format_string)

    return data

  def Write(self, values, format_string=None):

















    if not format_string:
      format_string = self.write_format
    try:
      Write(self.link, values, format_string)
    except OSError as e:
      if self.error_pin:
        messageboard.UpdateStatusLight(self.error_pin, True)
      self.Reopen(log_message='Failed to write: %s' % e)
      self.Write(values)














def RunCommand(cmd, sleep_seconds=1, log=True):

  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):
  """Attempts to make connections for a number of attempts, exiting program on fail."""
















  rfcomm_device, bt_mac_address = connection_tuple

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

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

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


    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' % (desc, b))
          return link
      Log('No handshake received at %s after %d seconds on attempt %d'
          % (desc, timeout, attempt))
    except OSError as e:
      Log('Handshake error with %s on attempt %d: %s' % (desc, 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):
  """Finds a USB device with Arduino as its mfg and returns an open connection to it.



  Args:
    baud: The connection speed.
    timeout: Max duration in seconds that we should wait to establish a connection.







  Returns:
    Open link to the Arduino if found and opened; None otherwise.
  """
  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




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




  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(
    open_function, link, connection_tuple, baud=9600, timeout=3, log_message=None):
  """Close and reopen the serial."""
  if log_message:
    Log(log_message)
  link.close()
  link = open_function(connection_tuple, baud=baud, timeout=timeout)
  return link


def StuffTuple(link, t):
  start_pos = 0
  for v in t:
    start_pos = link.tx_obj(v, start_pos)
  return start_pos


def Write(link, values, format_string):
  """Sends the encapsulated string command on an open pySerialTransfer."""
  packed_bytes = struct.pack(format_string, *values)
  print(format_string, values, packed_bytes)
  for n, b in enumerate(packed_bytes):
    link.txBuff[n] = b
  
  #link.tx_obj(b)
  link.send(len(packed_bytes))
  Log('Sent %s' % str(values))


def Write2(link, values, unused_format_string):
  """Sends the encapsulated string command on an open pySerialTransfer."""
  byte_count = StuffTuple(link, values)
  link.send(byte_count)
  Log('Sent %s' % str(values))


def ReadBytes(link):

  read_bytes = []
  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])
  return read_bytes


def Unpack(read_bytes, format_string):

  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 only %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):

  read_bytes = ReadBytes(link)
  data = ()
  if read_bytes:
    data = Unpack(read_bytes, format_string)
  return data


def AzimuthAltitude(flight):
  """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.



  Returns:
    Returns a tuple of the azimuth and altitude, in degrees, at the current system time.
  """
  geo_time = flight.get('flight_loc_now')
  lat = flight.get('lat')
  lon = flight.get('lon')
  speed = flight.get('speed')






  altitude = flight.get('altitude')
  track = flight.get('track')

  unused_vertrate = flight.get('vertrate')


  if all([isinstance(x, numbers.Number) for x in (lat, lon, speed, altitude, track)]):
    elapsed_time = time.time() - geo_time


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
















    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)

  return None


def DrainQueue(q):


  while not q.empty():
    value = q.get()
  return value


def InitialMessageValues(q):

  v = DrainQueue(q)
  if v:
    return v
  return {}, {}


def ServoMain(to_arduino_q, unused_to_parent_q):





  Log('Process started with process id %d' % os.getpid())








  link = Serial(
      OpenBluetooth, (RFCOMM_NUMBER_SERVO, HC05_MAC_ADDRESS_SERVO), read_timeout=5,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION)

  link.Open()


  servo_precision = 1

  #servos = OpenUSB(sn=SN_SERVO)
  flight = {}
  altitude = 0
  azimuth = 0

  flight, json_desc_dict = InitialMessageValues(to_arduino_q)




  while True:
    if not to_arduino_q.empty():
      flight, unused_json_desc_dict = to_arduino_q.get()











    link.Read('?')  # simple ack message sent by servos




    current_angles = AzimuthAltitude(flight)
    if current_angles:
      azimuth_change = azimuth - current_angles[0]
      altitude_change = altitude - current_angles[1]






      if abs(azimuth_change) > servo_precision or abs(altitude_change) > servo_precision:

        (azimuth, altitude) = current_angles

        values = (azimuth, altitude)
        link.Write(values)

    time.sleep(COMMAND_DELAY_TIME)




def SendAndWaitAck(
    remote,
    str_to_send,
    signature=None,
    timeout_time=3,
    max_send_attempts=3):
  """Sends a string to the open remote and optionally verifies receipt via a response.

  This sends a string to the remote. If a response is requested, this will attempt
  potentially transmits as requested, waiting after each transmit some number of seconds
  for a response that includes at minimum the key-value pair ack=signature (so the full
  response would be <ack=signature;>).



  Args:
    remote: open pySerialTransfer connection to the Arduino.
    str_to_send: string to send.
    signature: if a confirmation response is requested, the numeric value expected in that
      response.
    timeout_time: the max number of seconds per send attempt to wait for a response.
    max_send_attempts: the maximum number of times we will attempt to send.






  Returns:
    Boolean indicating whether the send attempt was successful (True) or not (False).
  """
  byte_count = StuffStr(remote, str_to_send)

  send_attempt = 0
  response = None
  first_send_time = time.time()

  if not SIMULATE_ARDUINO:
    while not response and send_attempt < max_send_attempts:
      remote.send(byte_count)
      send_attempt += 1
      response_start_time = time.time()
      while not response and time.time() - response_start_time < timeout_time:
        response = ReadArduinoSerial(remote)
      response_stop_time = time.time()

    if VERBOSE:
      if response:
        print('Send attempts: %d; round trip %f; listening time on final receipt: %f' % (
            send_attempt,
            (response_stop_time - first_send_time),
            (response_stop_time - response_start_time)))
      else:
        print('No response received after send attempt %d; total time elapsed: %f' % (
            send_attempt,
            (response_stop_time - first_send_time)))

    if signature is not None:
      if not response:
        if VERBOSE:
          print('Acknowledgment required yet none received\n')
        return False
      d = ParseArduinoResponse(response)
      if d is None:
        if VERBOSE:
          print('Improperly formatted response string: %s\n' % response)
        return False
      if 'ack' not in d:
        if VERBOSE:
          print('Response does not have an acknowledgement "ack" key: %s\n' % response)
        return False
      signature_received = d['ack']
      if signature_received != signature:
        if VERBOSE:
          print(
              'Acknowledgement code %s does not match expected value %s\n'
              % (signature_received, signature))
        return False

  return True









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 DesiredState(settings, flight, display_mode):
  """Generates a dictionary defining the target state for the remote.

  Generates the complete set of key-value pairs that describe the desired state for
  the remote.







  Args:




















































    settings: dictionary representing the current state of the messageboard configuration.
    flight: dictionary describing the most recent flight.
    display_mode: integer specifying what display mode - and thus what attributes about
      the flight or other attributes - should be sent.

  Returns:
    Dictionary describing the full state desired for the remote.

  """
  # Dictionary to send to remote
  d = {}
  d['setting_max_distance'] = settings['setting_max_distance']
  d['setting_max_altitude'] = settings['setting_max_altitude']

  d['setting_screen_enabled'] = 0
  if 'setting_screen_enabled' in settings:
    d['setting_screen_enabled'] = 1

  d['setting_on_time'] = settings['setting_on_time']
  d['setting_off_time'] = settings['setting_off_time']
  d['setting_delay'] = settings['setting_delay']
  now = flight.get('now')  # time flight was seen


  line1_decimal_mask = ''
  line2_decimal_mask = ''
  if display_mode == DISP_LAST_FLIGHT_NUMB_ORIG_DEST:
    # UAL1827 / SFO-LAX
    line1 = flight.get('flight_number', '')
    origin = ReplaceWithNA(flight.get('flight_origin', ''))
    destination = ReplaceWithNA(flight.get('flight_destination', ''))



    line2 = '%s-%s' % (origin, destination)

  elif display_mode == DISP_LAST_FLIGHT_AZIMUTH_ELEVATION:
    # AZM+193.1 / ALT +79.3
    current_angles = AzimuthAltitude(flight)



    if current_angles:
      (azimuth, altitude) = current_angles
      line1 = 'AZM%s' % FloatToAlphanumericStr(azimuth, 1, 5, True)
      line1_decimal_mask = '00000010'
      line2 = 'ALT%s' % FloatToAlphanumericStr(altitude, 1, 5, True)
      line2_decimal_mask = '00000010'
    else:
      line1 = KEY_NOT_PRESENT_STRING
      line2 = KEY_NOT_PRESENT_STRING

  elif display_mode == DISP_FLIGHT_COUNT_LAST_SEEN:
    # 18 TODAY / T+ 14H
    line1 = '%2d TODAY' % flight.get('flight_count_today', 0)
    elapsed_time_str = 'UNK'
    if now:
      elapsed_time_str = SecondsToShortString(time.time() - now)
    line2 = 'T+ %s' % elapsed_time_str

  elif display_mode == DISP_RADIO_RANGE:
    # RNG 87MI / 9 PLANES
    radio_range = flight.get('radio_range_miles')
    radio_range_str = KEY_NOT_PRESENT_STRING
    if radio_range is not None:
      radio_range_str = '%2dMI' % round(radio_range)
    line1 = 'RNG %s' % radio_range_str
    radio_range_flights = flight.get('radio_range_flights', 0)
    plural = ''
    if radio_range_flights == 1:
      plural = 'S'
    line2 = '%d PLANE%s' % (radio_range_flights, plural)











  d['line1'] = line1
  d['line2'] = line2
  d['line1_decimal_mask'] = line1_decimal_mask
  d['line2_decimal_mask'] = line2_decimal_mask

  return d


def StateToString(desired_state, current_state, ack=0):
  """Transforms the desired state dictionary to a command string.

  The dict describing key-value pairs is converted into a single command string in the
  following way:
  - key-value pairs matching that which are already known to the remote are not sent
  - an optional additional key-value pair of ack=random_int is added if a response from
    the remote is expected confirming receipt
  - format is transformed to an alphabetized list of <key1=value1;...;keyn=valuen;>

  Args:
    desired_state: Dictionary describing desired end state of remote.
    current_state: Dictionary describing current known state of remote.
    ack: Integer indicating whether the number of digits desired for an acknowledgement
      number; 0 indicates no acknowledgement requested.

  Returns:
    String as described above.
  """
  s = {}

  for key in desired_state:
    if desired_state[key] != current_state.get(key):
      s[key] = desired_state[key]

  signature = None
  if ack:
    signature = random.randrange(1, 10**ack)
    s['ack'] = signature

  kv_str = messageboard.BuildSettings(s)
  cmd_str = '%s%s;%s' % (COMMAND_START_CHAR, kv_str, COMMAND_END_CHAR)

  if len(cmd_str) > MAX_CMD_LENGTH - 1:  # the -1 is because C appends \0 to a string
    Log('Command to remote has len %d; max length is %d: %s' % (
        len(cmd_str)+1, MAX_CMD_LENGTH, cmd_str))

  return (cmd_str, signature)


def ParseArduinoResponse(s):
  """Transforms a command string of form <key1=value1;...;keyn=valuen;> into a dict.

  Args:
    s: command string.

  Returns:
    Dictionary of the command string.
  """
  if s[0] == COMMAND_START_CHAR and s[-1] == COMMAND_END_CHAR:
    return messageboard.ParseSettings(s[1:-1])

  Log('Invalid remote response: %s' % s)
  return None


def ExecuteArduinoCommand(command_str, settings, display_mode):
  """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
  This function parses the command, in the form <key1=value1;...;keyn=valuen;>, and then
  executes as requested.

  Args:
    command_str: string response, including the start and end markers, from the remote.
    settings: dictionary representing the current state of the messageboard configuration.
    display_mode: integer specifying what display mode - and thus what attributes about
      the flight or other attributes - should be sent.




  Returns:
    The (potentially updated) display_mode.

  """

  if not(command_str[0] == COMMAND_START_CHAR and command_str[-1] == COMMAND_END_CHAR):
    if VERBOSE:
      Log('Invalid remote command: %s' % command_str)
      print('Invalid command - missing delimiters: %s' % command_str)
      return display_mode

  command = messageboard.ParseSettings(command_str[1:-1])

  # command might update a setting; see if there's a change, and if so, write to disk
  setting_change = False
  # remote sees on/off as 1/0 whereas messageboard.py & the web interface expect on/off
  if 'setting_screen_enabled' in command:
    arduino_screen_enabled = command['setting_screen_enabled']
    if arduino_screen_enabled:
      command['setting_screen_enabled'] = 'on'
    else:
      command['setting_screen_enabled'] = 'off'
  setting_keys = ['setting_screen_enabled', 'setting_max_distance', 'setting_max_altitude',
                  'setting_on_time', 'setting_off_time', 'setting_delay']
  for key in setting_keys:
    if key in command and command[key] != settings[key]:


      setting_change = True

  if setting_change:
    for key in setting_keys:
      if key in command:
        settings[key] = command[key]
        if VERBOSE:
          print(
              ' |-->Updated %s to %s in %s' %
              (key, command[key], messageboard.CONFIG_FILE))
    messageboard.WriteFile(messageboard.CONFIG_FILE, messageboard.BuildSettings(settings))

  # 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 'last_plane' in command:
    messageboard.WriteFile(messageboard.LAST_FLIGHT_FILE, '')
    if VERBOSE:
      print(' |-->Requested last flight (re)display')

  # a command might request a histogram; simply generate and save a histogram file to disk
  if 'histogram' in command:
    histogram = {}
    histogram['type'] = 'messageboard'
    histogram['histogram'] = command['histogram']
    histogram['histogram_history'] = command['histogram_history']
    histogram['histogram_max_screens'] = '_1'  # default value since not provided
    histogram['histogram_data_summary'] = 'off'  # default value since not provided
    kv_string = messageboard.BuildSettings(histogram)
    messageboard.WriteFile(messageboard.HISTOGRAM_CONFIG_FILE, kv_string)
    if VERBOSE:
      print(' |-->Requested %s histogram with %s data' % (
          command['histogram'], command['histogram_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' in command:
    display_mode = command['display_mode']
    if VERBOSE:

      print(' |-->Display mode updated to %d (%s)' % (
          display_mode, DISPLAY_MODE_NAMES[display_mode]))







  if VERBOSE:
    print()



  return display_mode


def SecondsToShortString(seconds):
  """Converts a number of seconds to a three-character time representation (i.e.: 87M).

  Converts seconds to a three character representation containing at most two digits (1-99)
  and one character indicating time unit (S, M, H, or D).

  Args:
    seconds: Number of seconds.

  Returns:
    String as described.
  """
  s = round(seconds)
  if s < 99:
    return '%2dS' % s

  m = round(seconds / messageboard.SECONDS_IN_MINUTE)
  if m < 99:




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




    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 RemoteMain(to_arduino_q, to_parent_q):






  Log('Process started with process id %d' % os.getpid())

  # setting_screen_enabled: ?
  # setting_max_distance: h
  # setting_max_altitude: h
  # setting_on_time: h
  # setting_off_time: h
  # setting_delay: h
  # last_plane: ?
  # display_mode: h
  # histogram_enabled: ?
  # current_hist_type: 20s
  # current_hist_history: 20s
  format_string = '?hhhhh?h?20s20s'





























  link = Serial(
      OpenBluetooth, (RFCOMM_NUMBER_REMOTE, HC05_MAC_ADDRESS_REMOTE), read_timeout=5,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION,
      format_string=format_string)


  remote = OpenUSB(sn=SN_REMOTE)
  current_state = {}
  settings_next_read = 0
  flight_next_read = 0
  display_mode = 1
  simulation_counter = 0
  flight = {}
  signature = None
  if SIMULATE_ARDUINO:
    potential_commands = messageboard.ReadFile(SIMULATION_FILE).splitlines()

  while True:



    # Read config settings once per second
    if time.time() > settings_next_read:
      settings = messageboard.ReadAndParseSettings(messageboard.CONFIG_FILE)
      settings_next_read = time.time() + SETTINGS_READ_TIME
      if VERBOSE:
        print('Read %s' % messageboard.CONFIG_FILE)

    if time.time() > flight_next_read:
      flight = messageboard.ReadAndParseSettings(messageboard.ARDUINO_FILE)
      flight_next_read = time.time() + LATEST_FLIGHT_READ_TIME
      if VERBOSE:
        print('Read %s' % messageboard.ARDUINO_FILE)

    # Open connection
    if not remote:
      remote = OpenUSB(sn=SN_REMOTE)
      current_state = {}
      if VERBOSE:
        print('Reopening connection')
    if remote:
      desired_state = DesiredState(settings, flight, display_mode)
      if desired_state != current_state:
        ack = 4
        if SIMULATE_ARDUINO:
          ack = None
        (command_string, signature) = StateToString(desired_state, current_state, ack=ack)
        success = SendAndWaitAck(
            remote,
            command_string,
            signature=signature,
            timeout_time=3,
            max_send_attempts=3)
        if success:
          current_state.update(desired_state)

      if SIMULATE_ARDUINO:
        command = ''
        if potential_commands:
          simulation_counter += 1
          command = SimulateCommand(
              potential_commands, simulation_counter, randomized=False)
          if command:
            print('RECD (SIM): %s' % command)
      else:
        command = ReadArduinoSerial(remote)
      if command:
        display_mode = ExecuteArduinoCommand(command, settings, display_mode)


















    time.sleep(COMMAND_DELAY_TIME)




01234567890123456789012345678901234567890123456789012345678901234567890123456789
123 4  56789101112131415161718192021222324  252627282930313233343536373839404142434445464748495051  5253545556575859 60616263646566676869 707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442








446447448449450451452453454455456457458459460461462463464465       466467468 469470  471        472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571   572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609    610611  612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646      647648649650651       652653654655656657658659660661662663664665666667668669670671672673674675676677                              678679680681           682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792            793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862                                                        863864865866867868869870871 872873874875876877878879880881882883884885         886887888889  890891892893894895896897898899900901902903      904905906907908909910911 912913914915916917918919       920921922 923  924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958








100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099    1100     1101110211031104110511061107    11081109111011111112 111311141115111611171118111911201121                      11221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
#!/usr/bin/python3

import numbers

import os


import random
import struct
import subprocess
import sys
import time


import psutil
import serial
import serial.tools.list_ports
from pySerialTransfer import pySerialTransfer

import messageboard


VERBOSE = True
ARDUINO_LOG = 'arduino_log.txt'
SERIALS_LOG = 'arduino_serials_log.txt'
LOG_SERIALS = False
SIMULATE_ARDUINO = False



SERVO_SIMULATED_IN = 'servo_in.txt'
SERVO_SIMULATED_OUT = 'servo_out.txt'
REMOTE_SIMULATED_IN = 'remote_in.txt'
REMOTE_SIMULATED_OUT = 'remote_out.txt'

RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'
if RASPBERRY_PI:
  ARDUINO_LOG = messageboard.MESSAGEBOARD_PATH + ARDUINO_LOG
  SERIALS_LOG = messageboard.MESSAGEBOARD_PATH + SERIALS_LOG
  SERVO_SIMULATED_OUT = messageboard.MESSAGEBOARD_PATH + SERVO_SIMULATED_OUT
  SERVO_SIMULATED_IN = messageboard.MESSAGEBOARD_PATH + SERVO_SIMULATED_IN
  REMOTE_SIMULATED_OUT = messageboard.MESSAGEBOARD_PATH + REMOTE_SIMULATED_OUT
  REMOTE_SIMULATED_IN = messageboard.MESSAGEBOARD_PATH + REMOTE_SIMULATED_IN

CONNECTION_FLAG_BLUETOOTH = 1
CONNECTION_FLAG_USB = 2
CONNECTION_FLAG_SIMULATED = 3
RASPBERRY_PI = psutil.sys.platform.title() == 'Linux'

SN_SERVO = '5583834303435111C1A0'  # arduino mega serial
SERVO_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (2, '98:D3:11:FC:42:16'))

SN_REMOTE = '75835343130351802272'  # arduino uno serial
REMOTE_CONNECTION = (CONNECTION_FLAG_BLUETOOTH, (1, '98:D3:91:FD:B3:C9'))

MIN_ALTITUDE = 5  # below this elevation degrees, turn off the tracking



if SIMULATE_ARDUINO:
  SERVO_CONNECTION = (CONNECTION_FLAG_SIMULATED, (SERVO_SIMULATED_IN, SERVO_SIMULATED_OUT))
  REMOTE_CONNECTION = (
      CONNECTION_FLAG_SIMULATED, (REMOTE_SIMULATED_IN, REMOTE_SIMULATED_OUT))

KEY_NOT_PRESENT_STRING = 'N/A'


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'),
    (3, 'destination'),
    (4, 'airline'),
    (5, 'aircraft'),
    (6, 'altitude'),
    (7, 'bearing'),
    (8, 'distance'),
    (9, 'day_of_week'),
    (10, 'all'))

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 file is None:
    file = messageboard.LOGFILE
  if ser:
    additional_text = str(ser)
  else:
    additional_text = 'ARDUINO'

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


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:
      self.to_parent_q.put(('pin', (self.error_pin, True)))

    self.start_time = time.time()

  def __str__(self):
    return '%s @ %s opened @ %s' % (
        self.name, str(self.connection_tuple),
        messageboard.EpochDisplayTime(self.start_time))

  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, 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,
        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):
    """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)))

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

    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)
    except OSError as e:
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True)))
      self.Reopen(log_message='Failed to read: %s' % e)
      return self.Read(format_string=format_string)
    self.last_read = time.time()
    if data:
      self.last_receipt = time.time()
      if LOG_SERIALS:
        ts = time.time() - self.start_time
        str_data = str(['%7.2f' % d if isinstance(d, float) else str(d) for d in data])
        with open(SERIALS_LOG, 'a') as f:
          f.write('%10.3f RECD@%s: %s\n' % (ts, self.name, str_data))
    if self.read_timeout and self.last_read - self.last_receipt > self.read_timeout:
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True)))
      self.Reopen(log_message='Heartbeat not received in %.2f seconds'
                  % (self.last_read - self.last_receipt))
      return self.Read(format_string=format_string)

    return data

  def Write(self, values, format_string=None):
    """Writes to an open serial.

    Writes to an open serial values as identified in the format_string provided here,
    or if not provided in this call, as saved on the Serial instance.  If an OSError
    exception is detected, this method will attempt to reopen the connection.

    Args:
      values: tuple of values to send matching that as identified in format_string.
      format_string: String of the form expected by struct.pack
    """
    ts = time.time() - self.start_time
    str_values = str(['%7.2f' % v if isinstance(v, float) else str(v) for v in values])
    if self.connection_type == CONNECTION_FLAG_SIMULATED:
      with open(self.connection_tuple[1], 'a') as f:
        f.write('%10.3f: %s\n' % (ts, str_values))
      return

    if not format_string:
      format_string = self.write_format
    try:
      Write(self.link, values, format_string)
    except OSError as e:
      if self.error_pin:
        self.to_parent_q.put(('pin', (self.error_pin, True)))
      self.Reopen(log_message='Failed to write: %s' % e)
      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):
  """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 2-tuple of the rfcomm_device number (i.e.: the 1 in /dev/rfcomm1)
      and mac_address of the bluetooth radio; both 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.

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

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

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

  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' % (desc, b))
          return link
      Log('No handshake received at %s after %d seconds on attempt %d'
          % (desc, timeout, attempt))
    except OSError as e:
      Log('Handshake error with %s on attempt %d: %s' % (desc, 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




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




  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(
    open_function, link, connection_tuple, baud=9600, timeout=3, log_message=None):
  """Close and reopen the serial."""
  if log_message:
    Log(log_message)
  link.close()
  link = open_function(connection_tuple, baud=baud, timeout=timeout)
  return link









def Write(link, values, format_string):
  """Sends the encapsulated string command on an open pySerialTransfer."""
  packed_bytes = struct.pack(format_string, *values)

  for n, b in enumerate(packed_bytes):
    link.txBuff[n] = b


  link.send(len(packed_bytes))










def ReadBytes(link):
  """Reads the bytes at the link, returning a byte object."""
  read_bytes = []
  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])
  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):
  """Read and unpacks the bytes in one go."""
  read_bytes = ReadBytes(link)
  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)

  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 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.
  """
  Log('Process started with process id %d' % os.getpid())

  # Ensures that the child can exit if the parent exits unexpectedly
  # docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue.cancel_join_thread
  to_arduino_q.cancel_join_thread()
  to_parent_q.cancel_join_thread()

  # write_format: azimuth, altitude, R, G, & B intensity
  # read heartbeat: millis
  link = Serial(
      *SERVO_CONNECTION, read_timeout=5,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION, to_parent_q=to_parent_q,
      read_format='l', write_format='ffhhh', name='Servo')
  link.Open()





  last_flight = {}
  last_angles = (0, 0)


  flight, json_desc_dict, settings, additional_attr = InitialMessageValues(to_arduino_q)
  next_read = time.time() + READ_DELAY_TIME
  next_write = time.time() + WRITE_DELAY_TIME
  now = GetNow(json_desc_dict, additional_attr)

  while not shutdown.value:
    if not to_arduino_q.empty():
      flight, json_desc_dict, settings, additional_attr = to_arduino_q.get(block=False)
      new_flight = DifferentFlights(flight, last_flight)

      if new_flight:
        Log('Flight changed: %s' % 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_RGB_OFF))

      last_flight = flight

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

    now = GetNow(json_desc_dict, additional_attr)

    current_angles = AzimuthAltitude(flight, now)
    if current_angles and time.time() > next_write:
      if current_angles[1] >= settings['minimum_altitude_servo_tracking']:
        laser_rgb = LaserRGBFlight(flight)
        link.Write((*current_angles, *laser_rgb))
        last_angles = current_angles  # stop moving the head at this point
      else:
        link.Write((*last_angles, *LASER_RGB_OFF))
      next_write = time.time() + WRITE_DELAY_TIME

    if not SIMULATE_ARDUINO:






      time.sleep(READ_DELAY_TIME)
  link.Close()
  Log('Shutdown signal received by process %d' % os.getpid(), link)
  to_parent_q.put(('pin', (messageboard.GPIO_ERROR_ARDUINO_SERVO_CONNECTION, True)))









LASER_RGB_OFF = (0, 0, 0)
def LaserRGBFlight(flight):
  """Based on flight attributes, set the laser."""
  if not flight:
    return LASER_RGB_OFF
  return 1, 0, 0


def DifferentFlights(f1, f2):
  """True if flights same except for persistent path; False if they differ.

  We cannot simply check if two flights are identical by checking equality of the dicts,
  because a few attributes are updated after the flight is first found:
  - the persistent_path is kept current
  - cached_* attributes may be updated
  - the insights are generated after flight first enqueued
  On the other hand, we cannot check if they are identical by looking just at the
  flight number, because flight numbers may be unknown. Thus, this checks all
  attributes except the persistent path.

  Returns:
    List of the different keys, excluding the persistent_path key.
  """
  if f1 is None and f2 is None:
    return []






























  if f1 is None:
    return sorted(list(f2.keys()))
  if f2 is None:
    return sorted(list(f1.keys()))












  excluded_keys = ['persistent_path', 'insight_types']
  different_attributes = []
  for key in set(f1.keys()).union(set(f2.keys())):
    if key not in excluded_keys and not key.startswith(messageboard.CACHED_ELEMENT_PREFIX):
      if f1.get(key) != f2.get(key):
        different_attributes.append(key)

  return sorted(different_attributes)


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.
  """
  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)))
      values.append(d[k][:max_length])
    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, settings, 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.
    settings: dictionary representing the current state of the messageboard configuration.
    additional_attr: dictionary with miscellaneous attributes from messageboard.
    display_mode: integer specifying what 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.
  """












  now = flight.get('now')  # time flight was seen
  simulated_time = json_desc_dict['simulated_time']  # simulated or real time

  line1_decimal_mask = '00000000'
  line2_decimal_mask = '00000000'
  if display_mode == DISP_LAST_FLIGHT_NUMB_ORIG_DEST:
    # UAL1827 / SFO-LAX
    line1 = ''
    line2 = ''
    if flight:
      line1 = messageboard.DisplayFlightNumber(flight)
      origin = messageboard.DisplayOriginIata(flight)[:3]
      destination = messageboard.DisplayDestinationIata(flight)[:3]
      line2 = '%s-%s' % (origin, destination)

  elif display_mode == DISP_LAST_FLIGHT_AZIMUTH_ELEVATION:
    # AZM+193.1 / ALT +79.3
    current_angles = AzimuthAltitude(flight, GetNow(json_desc_dict, additional_attr))
    line1 = ''
    line2 = ''
    if flight:
      if current_angles:
        (azimuth, altitude) = current_angles
        line1 = 'AZM%s' % FloatToAlphanumericStr(azimuth, 1, 5, True)
        line1_decimal_mask = '00000010'
        line2 = 'ALT%s' % FloatToAlphanumericStr(altitude, 1, 5, True)
        line2_decimal_mask = '00000010'
      else:
        line1 = KEY_NOT_PRESENT_STRING
        line2 = KEY_NOT_PRESENT_STRING

  elif display_mode == DISP_FLIGHT_COUNT_LAST_SEEN:
    # 18 TODAY / T+ 14H
    line1 = '%2d TODAY' % json_desc_dict.get('flight_count_today', 0)
    elapsed_time_str = 'UNK'
    if now:
      elapsed_time_str = SecondsToShortString(simulated_time - now)
    line2 = 'T+ %s' % elapsed_time_str

  elif display_mode == DISP_RADIO_RANGE:
    # RNG 87MI / 9 PLANES
    radio_range = json_desc_dict.get('radio_range_miles')
    radio_range_str = KEY_NOT_PRESENT_STRING
    if radio_range is not None:
      radio_range_str = '%2dMI' % round(radio_range)
    line1 = 'RNG %s' % radio_range_str
    radio_range_flights = json_desc_dict.get('radio_range_flights', 0)
    plural = ''
    if radio_range_flights != 1:
      plural = 'S'
    line2 = '%d PLANE%s' % (radio_range_flights, plural)

  d = {}
  setting_screen_enabled = False
  if 'setting_screen_enabled' in settings:
    setting_screen_enabled = True
  d['setting_screen_enabled'] = setting_screen_enabled
  d['setting_max_distance'] = settings['setting_max_distance']
  d['setting_max_altitude'] = settings['setting_max_altitude']
  d['setting_on_time'] = settings['setting_on_time']
  d['setting_off_time'] = settings['setting_off_time']
  d['setting_delay'] = settings['setting_delay']
  d['line1'] = line1
  d['line2'] = line2
  d['line1_dec_mask'] = int(line1_decimal_mask, 2)
  d['line2_dec_mask'] = int(line2_decimal_mask, 2)

  return d


























































def ExecuteArduinoCommand(command, settings, 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.
    settings: dictionary representing the current state of the messageboard configuration.
    display_mode: current display mode; only passed so that we may identify changes.
    low_battery: current battery status; only passed so that we may identify changes.
    to_parent_q: multiprocessing queue, where instructions to send back to messageboard,
      if any, can be placed.
    link: the open serial link.

  Returns:
    A 3-tuple of potentially-updated display_mode, low_battery, the updated
    settings dictionary.
  """









  # command might update a setting; see if there's a change, and if so, write to disk
  setting_change = False
  # remote sees T/F whereas messageboard.py & the web interface expect 'on'/absent key
  if command['setting_screen_enabled_bool']:


    command['setting_screen_enabled'] = 'on'

  log_lines = []
  setting_keys = ['setting_screen_enabled', 'setting_max_distance', 'setting_max_altitude',
                  'setting_on_time', 'setting_off_time', 'setting_delay']
  for key in setting_keys:
    if command.get(key) != settings.get(key):
      log_lines.append(' |-->Setting %s updated from %s to %s' % (
          key, str(settings[key]), str(command[key])))
      setting_change = True
      settings[key] = command[key]
  if setting_change:
    settings_string = messageboard.BuildSettings(settings)
    to_parent_q.put(('update_settings', (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]))

  # a command might tell us the battery is low
  to_parent_q.put(
      ('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, command['low_battery'])))
  if low_battery != command['low_battery']:
    log_lines.append(' |-->Low battery set to %d' % command['low_battery'])

  if VERBOSE:
    if log_lines:
      log_lines.insert(0, '')  # for improved formatting
      Log('\n'.join(log_lines), link)

  return command['display_mode'], command['low_battery'], settings, setting_change


def SecondsToShortString(seconds):
  """Converts a number of seconds to a three-character time representation (i.e.: 87M).

  Converts seconds to a three character representation containing at most two digits (1-99)
  and one character indicating time unit (S, M, H, or D).

  Args:
    seconds: Number of seconds.

  Returns:
    String as described.
  """
  s = round(seconds)
  if s < 99:
    return '%2dS' % s

  m = round(seconds / messageboard.SECONDS_IN_MINUTE)
  if m < 99:




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




    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 of two-tuples, returns two tuples, where each has respective elements."""
  k = tuple([t[0] for t in config])
  f = tuple([t[1] for t in config])
  return k, f


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 settings; and executes any commands
  such as histogram requests or setting updates.
  """
  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',        'h'),
      ('setting_on_time',             'h'),
      ('setting_off_time',            'h'),
      ('setting_delay',               'h'),
      ('last_plane',                  '?'),
      ('display_mode',                'h'),
      ('histogram_enabled',           '?'),
      ('current_hist_type',           'h'),
      ('current_hist_history',        'h'),
      ('low_battery',                 '?'))

  write_config = (
      ('setting_screen_enabled',      '?'),
      ('setting_max_distance',        'h'),
      ('setting_max_altitude',        'h'),
      ('setting_on_time',             'h'),
      ('setting_off_time',            'h'),
      ('setting_delay',               'h'),
      ('line1',                       '8s'),
      ('line2',                       '8s'),
      ('line1_dec_mask',              'h'),
      ('line2_dec_mask',              'h'))
  #pylint: enable = bad-whitespace
  read_keys, read_format = SplitFormat(read_config)
  write_keys, write_format = SplitFormat(write_config)

  values_d = {}
  low_batt = False
  to_parent_q.put(('pin', (messageboard.GPIO_ERROR_BATTERY_CHARGE, low_batt)))

  link = Serial(
      *REMOTE_CONNECTION, read_timeout=5,
      error_pin=messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, to_parent_q=to_parent_q,
      read_format=read_format, write_format=write_format, name='Remote')
  link.Open()





  display_mode = 0






  flight, json_desc_dict, settings, additional_attr = InitialMessageValues(to_arduino_q)
  next_read = time.time() + READ_DELAY_TIME
  next_write = time.time() + WRITE_DELAY_TIME

  ignore_parent_settings = False
  updated_settings = settings





  while not shutdown.value:
    if not to_arduino_q.empty():
      to_arduino_message = to_arduino_q.get(block=False)
      flight, json_desc_dict, settings, additional_attr = to_arduino_message


      # There is a tricky parallel process timing issues: the remote has more up-to-date
      # settings for a brief interval until the parent has chance to write the settings;
      # all already-queued messages (perhaps at most one) will still have the old settings
      # prior to any updates from the remote. Thus, we need to use the updated settings
      # from the remote, ignoring the settings from the parent, until such time as the
      # parent settings match once again the settings on the remote.
      if settings == updated_settings:
        ignore_parent_settings = False






















      else:
        Log('Settings from main out of date; ignoring')
      if ignore_parent_settings:
        settings = updated_settings

    if time.time() >= next_write:
      message_dict = GenerateRemoteMessage(
          flight, json_desc_dict, settings, additional_attr, display_mode)
      message_tuple = DictToValueTuple(message_dict, write_keys, write_format)
      link.Write(message_tuple)
      next_write = time.time() + WRITE_DELAY_TIME

    if time.time() >= next_read:
      values_t = link.Read()  # simple ack message sent by servos
      values_d = dict(zip(read_keys, values_t))
      if values_d.get('confirmed'):
        results = ExecuteArduinoCommand(
            values_d, settings, display_mode, low_batt, to_parent_q, link)
        display_mode, low_batt, updated_settings, ignore_parent_settings = results
      next_read = time.time() + READ_DELAY_TIME

    if not SIMULATE_ARDUINO:
      time.sleep(READ_DELAY_TIME)
  link.Close()
  Log('Shutdown signal received by process %d' % os.getpid(), link)
  to_parent_q.put(('pin', (messageboard.GPIO_ERROR_ARDUINO_REMOTE_CONNECTION, True)))