#! /usr/bin/python3
# Last edited on 2018-07-06 17:50:49 by stolfilocal

# {muff_arduino.py}: Library module for interfacing with the
# Arduino controller of the MUFF v2.0 microscope positioner.

import os, sys, serial, time
from sys import stderr 

num_LEDs = 24     # Number of LEDs; named 'A', 'B', etc.
verbose = False   # If true, prints lots of debugging info.

# Most functions in this module take a {sport} argument
# which is the serial port object connected to the Arduino.

# If {sport = None}, the functions just pretend that 
# the Arduino is there and responding as expected.
# This is useful when debugging this module and the 
# Arduino is not available.

def connect(dummy,verb):
  """Open a serial port to the Arduino and returns it.
  
  However, if {dummy} is true, skips and returns {None}
  instead.
  
  If {verb} is true, turns on verbose mode for all functions in
  this module."""
  
  global verbose
  verbose = verb
  if dummy:
    return None
  else:
    sport = serial.Serial \
      ( "/dev/ttyUSB0", 9600,
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE )
    time.sleep(2)
    return sport
# ----------------------------------------------------------------------
  
def start_motor(sport,dir):
  """Send command to the Arduino to start moving the microscope 
  in the direction {dir} (+1 = up, -1 = down)."""
  
  # IN case the motor is moving, stop it:
  stop_motor(sport)
  if dir == +1:
    command = b"1" # Start moving up.
  elif dir == -1:
    command = b"2" # Start moving down.
  else:
    assert False
  if verbose: 
    stderr.write("[muff_adruino:] starting motor")
    stderr.write(" in direction %+d - to Arduino: '%s'\n" % (dir,show_bytes(command,False)))
  if sport != None: sport.write(command)
  wait_arduino_OK(sport)
# ----------------------------------------------------------------------
  
def stop_motor(sport):
  """Send command to the Arduino to stop the motor, in case it is moving
  because of a previous {start_motor}."""
  
  command = b"3"
  if verbose: stderr.write("[muff_adruino:] stopping motor - to Arduino: '%s'\n" % show_bytes(command,False))
  if sport != None: sport.write(command)
  wait_arduino_OK(sport)
# ----------------------------------------------------------------------
  
def set_Z_step(sport,Z_step):
  """Send command to the Arduino to define the Z increment
  to be {Z_step} millimeters."""
  
  istep = int(round(Z_step*1000)) # Step size in microns.
  assert (istep >= -999) and (istep <= +999) # Arduino expects sign and 3 digits.
  command = b"4%+03d" % istep;
  if verbose: stderr.write("[muff_adruino:] defining Z step - to Arduino: '%s'\n" % show_bytes(command,False))
  if sport != None: sport.write(command)
  wait_arduino_OK(sport)
# ----------------------------------------------------------------------
  
def move_microscope(sport):
  """Sends commands to the Arduino to raises the microscope 
  holder by the predefined Z step amount."""
  
  command = b'5'
  if verbose: stderr.write("[muff_adruino:] raising microscope - to Arduino: '%s'\n" % show_bytes(command,False))
  if sport != None: sport.write(command)
  wait_arduino_OK(sport)
  return
# ----------------------------------------------------------------------

def read_signif(sport):
  """Reads one character from the serial port {sport}, skipping blanks,
  end-of-lines (CR, NL) and comments (from '#' to end-of-line). 
  If {verbose} is true, echoes the character on {stderr}. 
  Returns the character as a {bytes} object.  
  
  Complains and aborts if the read fails. If {sport} is {None} 
  (debugging mode), returns b'0'."""

  if sport == None: return b'0'

  # Skip any unread characters:
  sport.flush()
  
  # Read until non-blank and non-comment, or error:
  while True:
    s = readchar(sport)
    if s == b'#':
      if verbose: stderr.write("[muff_adruino:] received from Arduino: '%s" % show_bytes(s,False));
      # Skip all data to end-of-line, echo on {stderr}:
      skip_to_eol(sport)
      if verbose: stderr.write("'\n")
    else:
      if verbose: stderr.write("[muff_adruino:] received from Arduino: '%s'\n" % show_bytes(s,True))
      if s != b' ' and s != b'\r' and s != b'\n':
        break
      else:
        # Ignore.
        pass
  return s
# ----------------------------------------------------------------------

def skip_to_eol(sport):
  """Reads characters from the serial port {sport} until the first end-of-line (CR or NL),
  echoing them on {stderr} if {verbose} is true.  If {sport} is {None} (debugging mode),
  retrns immediately."""
  
  if sport == None: return

  while True:
    c = readchar(sport);
    if verbose: stderr.write(show_bytes(c,False))
    if s == b'\r' or s == b'\n':
       break
# ----------------------------------------------------------------------

def readchar(sport):
  """Reads one character from the serial port {sport} and returns it as a
  {bytes} object. Complains and aborts if the port yielded empty or more
  than 1 character; otherwise does NOT echo the character on {stderr},
  even if {verbose} is true.  If {sport} is {None} (debugging mode),
  returns b'0'."""
  
  if sport == None: return b"0"
  
  while True:
    c = sport.read() # Returns a {bytes} object.
    if len(c) == 1:
      return c
    else:
      sys.stderr.write("\n[muff_arduino] ** sport.read() returned '%s' (len = %d)\n" % (show_chrs(c,False), len(c)));
      sys.stderr.write("\n[muff_arduino] ** aborted.\n")
# ----------------------------------------------------------------------

def show_bytes(s, blanks):
  """Given a {bytes} object {s}, returns a {string} object
  with each non-printing char in {s} replaced by '[chr({NNN})]',
  where {NNN} is the character's decimal {ord}.  Also replaces 
  quotes, brackets, parentheses. If {blanks} is true, 
  replaces blanks too."""
  
  n = len(s)
  res = ""
  bad = b"\'\"[]()" # Printable bytes that should be converted too.
  for i in range(n):
    c = s[i]
    if (c == b' '[0] and blanks) or (c < b' '[0]) or (c > b'~'[0]) or (c in bad):
      # Show chr code:
      res = res + ("[chr(%03d)]" % c)
    else:
      res = res + chr(c)
  return res
# ----------------------------------------------------------------------

def wait_arduino_OK(sport):
  """Waits for a b'0' command return code from the serial port {sport}.
  Ignores spaces and EOLs (NL, CR). If it receives a "#", ignores everything
  up to and including the EOL. Otherwise, if the return code is not "0",
  aborts with error.
  
  If {sport} is {None} (debugging mode), returns immediately."""
  
  if sport == None: 
    time.sleep(0.2)
    return
  else:
    s = read_signif(sport)
    if s != b'0':
      stderr.write("** [muff_arduino:] Invalid OK return code from Arduino: '%s'\n" % show_bytes(s,True))
      sys.exit(1)
  return 0
# ----------------------------------------------------------------------

def test_lights(sport):
  """Tests the LEDs by turning them all on, then
  turning them all off.  If {sport} is {None} (debugging mode),
  returns immedately."""
  
  if verbose: stderr.write("[muff_arduino:] testing the LEDs\n")
  switch_all_LEDs(sport,1.0)
  time.sleep(1.0)
  switch_all_LEDs(sport,0.0)
  time.sleep(0.1)
# ----------------------------------------------------------------------
 
def switch_LED(sport,lix,pwr):
  """Sends commands to the Arduino to turn the LED number {lix} on 
  with relative intensity {pwr}, which should be between 0.0 
  (off) and 1.0 (max intensity).
  
  Currently only works if {pwr} is 0 (Arduino command '-')
  or 1 (Arduino command '+').""" 
  
  assert type(lix) is int and lix >= 0 and lix < num_LEDs
  assert type(pwr) is float and pwr >= 0.0 and pwr <= 1.0
  
  lname = chr(ord("A") + lix).encode('ascii') # Name of LED for the Arduino.
  if pwr == 1.0:
    if verbose: stderr.write("[muff_arduino:] switching LED %02d on\n" % lix)
    if sport != None: sport.write(b'+'); sport.write(lname)
    wait_arduino_OK(sport)
  elif pwr == 0.0:
    if verbose: stderr.write("[muff_arduino:] switching LED %02d off\n" % lix)
    if sport != None: sport.write(b'-'); sport.write(lname)
    wait_arduino_OK(sport)
  else:
    if verbose: stderr.write("[muff_arduino:] switching LED %02d on with intensity %.2f\n" % (lix,pwr))
    stderr.write("** [muff_arduino:] partial LED intensity not implemented yet.\n")
    sys.exit(1)
# ----------------------------------------------------------------------
 
def switch_all_LEDs(sport,pwr):
  """Sends commands to the Arduino to turn all LEDs number on 
  with relative intensity {pwr}, which should be between 0.0 
  (off) and 1.0 (max intensity).
  
  Currently only works if {pwr} is 0 (Arduino command '-@')
  or 1 (Arduino command '+@').""" 
  
  assert type(pwr) is float and pwr >= 0.0 and pwr <= 1.0
  
  if pwr == 1.0:
    if verbose: stderr.write("[muff_arduino:] switching all LEDs on\n")
    if sport != None: sport.write(b'+@');
    wait_arduino_OK(sport)
  elif pwr == 0.0:
    if verbose: stderr.write("[muff_arduino:] switching all LEDs off\n")
    if sport != None: sport.write(b'-@');
    wait_arduino_OK(sport)
  else:
    if verbose: stderr.write("[muff_arduino:] switching all LEDs on with intensity %.2f\n" % pwr)
    stderr.write("** [muff_arduino:] partial LED intensity not implemented yet.\n")
    sys.exit(1)
# ----------------------------------------------------------------------

