#!python3
###############################################################################
# Source file : bd_server.py
# Language    : Python 3.3 or Python 2.7
# Author      : Kevin M. Hubbard 
# Description : A TCP Socket Server for Backdoor interfacing to hardware. 
#               This provides a common software interface (TCP Sockets) for 
#               executables and scripts to access Hardware without having 
#               device driver access to USB or PCIe. 
# License     : GPLv3
#      This program is free software: you can redistribute it and/or modify
#      it under the terms of the GNU General Public License as published by
#      the Free Software Foundation, either version 3 of the License, or
#      (at your option) any later version.
#
#      This program is distributed in the hope that it will be useful,
#      but WITHOUT ANY WARRANTY; without even the implied warranty of
#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#      GNU General Public License for more details.
#
#      You should have received a copy of the GNU General Public License
#      along with this program.  If not, see <http://www.gnu.org/licenses/>.
#                                                               
#       -------------             --------------            
#      |             |           |              |       
#      |  User App   |<-sockets->| bd_server.py |<-pyserialVCP-> USB Backdoor
#      |exe or script|           |              |<-FTDI D3xx---> FT600 Backdoor
#       -------------             --------------               
#
# PySerial for Python3 from:
#   https://pypi.python.org/pypi/pyserial/
# -----------------------------------------------------------------------------
# History :
#   01.23.2014 : khubbard : Created 
#   03.06.2014 : khubbard : Sleep feature to prevent CPU spinning on idle.
#   03.10.2014 : khubbard : Merged bd_server3.py and bd_server2.py into one.
#   03.12.2014 : khubbard : Fixed broken reads - problem with num_dwords
#   05.22.2014 : khubbard : Fixed backwards compatibility with Python2       
#   05.29.2014 : khubbard : Send !! after sleep to USB for HW power cycling  
#   05.29.2014 : khubbard : Dont crash on serial port timeouts
#   05.29.2014 : khubbard : Improved startup error handling.   
#   06.03.2014 : khubbard : USB Posted Writes for 16x performance   
#   06.03.2014 : khubbard : TCP Socket Write Combining    
#   06.03.2014 : khubbard : New repeat address Write Bursting ("W") command.
#   06.20.2014 : khubbard : More verbose error handling with import traceback
#   07.17.2014 : khubbard : Added "!!" support for unlocking Poke after reboot
#   07.18.2014 : khubbard : Added "rt" register test command
#   09.25.2014 : khubbard : Improved Sleep, Wake and !!      
#   07.21.2016 : khubbard : Added MesaBus support. Switch to ini config file
#   07.22.2016 : khubbard : Pad DWORDs on "k" read-multiple command with " ".
#   09.06.2016 : khubbard : Python 3.5 cast len(payload)/2 to int
#   10.10.2016 : khubbard : 2nd PCIe driver added
#   10.16.2016 : khubbard : "AUTO" option for usb_port for FTDI search.
#   2017.12.19 : khubbard : Upgraded for USB3 FT600 support.           
###############################################################################
import sys;
import select;
import socket;
import time;
import os;
from time import sleep;

#from SimpleXMLRPCServer import SimpleXMLRPCServer
#from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler

# Restrict to a particular path.
#class RequestHandler(SimpleXMLRPCRequestHandler):
#  rpc_paths = ('/RPC2',)
#def xmlrpc_function(x, y):
#  status = 1+x+y;
#  result = [5, 6, [4, 5]]
#  return (status, result)
# server = SimpleXMLRPCServer(( "157.226.13.36" , 8000),
# server = SimpleXMLRPCServer(("localhost", 8000),
# server = SimpleXMLRPCServer(("localhost", 21567),
#                           requestHandler=RequestHandler);
# server.register_function(xmlrpc_function);
# server.serve_forever();


def main():
  args = sys.argv + [None]*3;# Example "bd_server.ini"
  vers          = "2017.12.19";
  auth          = "khubbard";
  posted_writes = True;# USB Only - speeds up back2back writes 16x

  # If no ini file is specified in ARGS[1], look for bd_server.ini in CWD.
  file_name = os.path.join( os.getcwd(), "bd_server.ini");
  if ( args[1] != None and os.path.exists( args[1] ) ):
    file_name = args[1];

  # If it exists, load it, otherwise create a default one and then load it.
  if ( ( os.path.exists( file_name ) ) == False ):
    ini_list =  ["bd_connection   = usb     # usb,pi_spi",
                 "bd_protocol     = mesa    # mesa,poke",
                 "usb_port        = AUTO    # ie COM4 FT600",
                 "tcp_port        = 21567   # ie 21567",
                 "baudrate        = 921600  # ie 921600",
                 "mesa_slot       = 00      # ie 00",
                 "mesa_subslot    = 0       # ie 0",
                 "debug_bd_en     = 0       # ie 0,1",
                 "debug_tcp_en    = 0       # ie 0,1",  ];
    ini_file = open ( file_name, 'w' );
    for each in ini_list:
      ini_file.write( each + "\n" );
    ini_file.close();
    
  if ( ( os.path.exists( file_name ) ) == True ):
    ini_file = open ( file_name, 'r' );
    ini_list = ini_file.readlines();
    ini_hash = {};
    for each in ini_list:
      words = " ".join(each.split()).split(' ') + [None] * 4;
      if ( words[1] == "=" ):
        ini_hash[ words[0] ] = words[2];

  # Assign ini_hash values to legacy variables. Error checking would be nice.
  tcp_log_en = False;
  bd_log_en  = False;
  debug      = False;
  if ( ini_hash["debug_tcp_en"] == "1" ):
    tcp_log_en = True;
    tcp_log    = open ( 'bd_server_tcp_log.txt', 'w' );
  if ( ini_hash["debug_bd_en"] == "1" ):
    bd_log_en = True;
    debug     = True;
    bd_log    = open ( 'bd_server_bd_log.txt', 'w' );
  bd_type      = ini_hash["bd_connection"].lower();
  bd_protocol  = ini_hash["bd_protocol"].lower();
  com_port     = ini_hash["usb_port"];
  tcp_port     = int(ini_hash["tcp_port"],10);
  baudrate     = int(ini_hash["baudrate"],10);
  mesa_slot    = int(ini_hash["mesa_slot"],16);
  mesa_subslot = int(ini_hash["mesa_subslot"],16);
    
  os.system('cls');# Win specific clear screen
  print("------------------------------------------------------------------" );
  print("bd_server.py "+vers+" by "+auth+".");

  # Establish Python is version 2 or 3
  if ( sys.version_info[0] != 2 and sys.version_info[0] != 3 ):
    print(" [FAIL] bd_server.py running on Python"+str( sys.version_info[0] ));
    abort();
  print(" [OK]   bd_server.py running on Python"+str( sys.version_info[0] ));

  # Establish TCP Socket Connection
  try:
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM );
    server_socket.bind(('', tcp_port ));
    server_socket.listen(5);
    print(" [OK]   Connection to TCP Socket "+str(tcp_port)+" established." );
  except:
    print(" [FAIL] Connection to TCP Socket "+str(tcp_port) );
    abort();

  # Establish Hardware Connection
  try:
    if ( True ):
      if ( bd_protocol == "poke" ):
        # Old Way
        hw_bridge = Backdoor_Poke_UART( port_name=com_port, baudrate=baudrate,
                                        posted_writes=posted_writes );
#     else:
#       hw_bridge = Backdoor_Mesa_UART( port_name=com_port, baudrate=baudrate,
#                                       slot=mesa_slot, subslot=mesa_subslot );
#       com_port = hw_bridge.port_name;
#
      else:
        # New interface stack for MesaBus over either FT600 or FT232 USB link
        if ( com_port == "FT600" ):
          usb_link = usb_ft600_link( debug = debug );# USB3 over a FT600 Chip
          lf = "";
          cfg = usb_link.get_cfg();
          desired_freq = 1;# 1=66 MHz, 0=100 MHz
          if ( cfg.FIFOClock != desired_freq ):
            print("Changing FT600 Frequency");
            cfg.FIFOClock = desired_freq;
            rts = usb_link.set_cfg( cfg );
            usb_link.close();
            sleep(0.5);# Chip will reset, so must wait
            usb_link = usb_ft600_link();
        else:
          usb_link = usb_ft232_link( port_name=com_port, baudrate=baudrate,
                                     debug = debug );
          lf = "\n";
        mb  = mesa_bus( phy_link = usb_link, lf = lf, debug = debug );
        hw_bridge = lb_link( mesa_bus=mb, slot=0x00, subslot=0x0,debug=debug);

    print(" [OK]   Connection to " + bd_type + " " + com_port+" established." );
  except:
    print(" [FAIL] Connection to " + bd_type + " " + com_port);
    abort();

  print(" [OK]   bd_server.py is five-by-five." );
  print("------------------------------------------------------------------" );
  print( " bd_client <-> TCP Sockets <-> bd_server <->"+  \
          " "+com_port+" "+ "<-> Hardware " );
  print("------------------------------------------------------------------" );
  print("Press Ctrl+Break to terminate.");      




  ##############################
  # Main TCP Server Loop 
  ##############################
  read_list = [server_socket];
  run = True;
  # Sit in a loop listening for TCP traffic and build up a full packet per 
  # Backdoor Packet Protocol. Once a full packet is received, translate
  # it to a bus command, wait for a response and ACK back over TCP.
  # A packet begins with 8 ASCII hex nibbles, example "00000010" that indicate 
  # the number of bytes in the payload. Thank Nagle for that.
  # WARNING: Once a connection is established, this loop will spin up the CPU.
  # Prevent this from happening by calling sleep after a long idle period.
  idle_cnt = 0;
  idle_sleep_timeout  =  5000000; # About 15 seconds
  sleep_jk            =  True;
  while ( run == True ):
    readable, writable, errored = select.select(read_list, [], []);
    for s in readable:
      if s is server_socket:
        client_socket, address = server_socket.accept();
        read_list.append(client_socket);
        print(("Connection established from", address));
        packet_jk  = False;
        packet_len = 0;
        packet_buf = "";
      else:
        rx_str = s.recv(1024);
        if rx_str:
          idle_cnt = 0;
          if ( sleep_jk == True ):
            sleep_jk =  False;
            # As a courtesy ( not required ) after sleeping, send a Backdoor
            # unlock command in case the hardware was reset or power cycled
            if ( isinstance( hw_bridge, Backdoor_Poke_UART ) ):# Vs PCIe
              print("Waking");
              hw_bridge.ser.flushOutput();
              hw_bridge.ser.flushInput();
              hw_bridge.ser.write( "!!".encode("utf-8"));

          packet_buf += rx_str.decode("utf-8");# Conv Byte Array to String
          if ( tcp_log_en ):
            tcp_log.write( "RX:"+packet_buf+"\n" );
          if ( packet_jk == False and ( len( packet_buf ) > 8 ) ):
            try:
              packet_len = int(packet_buf[0:8],16);# "00000012" to 0x00000012
            except:
              print("!Es muy mala! - Packet Header is corrupted.");
            packet_buf = packet_buf[8:];# Remove Header
            packet_jk  = True;
          while ( packet_jk == True and ( len(packet_buf) >= packet_len ) ):
            payload    = packet_buf[0:packet_len];# An entire packet
            packet_buf = packet_buf[packet_len:]; # Any remainder (next packet)
            packet_jk  = False;
            packet_len = 0;

            # See if the remainder has enough to start new packet
            if ( packet_jk == False and ( len( packet_buf ) > 8 ) ):
              try:
                packet_len = int( packet_buf[0:8], 16 );
              except:
                print("!Es muy mala! - Packet Header is corrupted.");
              packet_buf = packet_buf[8:];# Remove Header
              packet_jk  = True;

            # Process the 1st Packet
            rts = process_payload( hw_bridge, payload );
            rts_size = len( rts );# How many bytes
            rts_size = "%08x" % rts_size;# 0x00000012 to "00000012"
            if ( bd_log_en ):
              bd_log.write( "RX:"+payload+"\n" );
              bd_log.write( "TX:"+rts+"\n" );
            if ( tcp_log_en ):
              tcp_log.write( "TX:"+rts_size+rts+"\n" );
            bin_data = ( rts_size + rts ).encode("utf-8");# 
            s.send( bin_data );# Send ACK back
          # while ( packet_jk == True and ( len(packet_buf) >= packet_len ) ):
        else:
          # Prevent CPU from staying spun up after a period of no TCP traffic
          if ( idle_cnt == (idle_sleep_timeout-1) ):
            print("Sleeping");
            sleep_jk = True;
          if ( idle_cnt == idle_sleep_timeout ):
            time.sleep(0.01);# 10ms sleep
          else:
            idle_cnt+=1;
        # if rx_str:
      # if s is server_socket:
    # for s in readable:
  # while ( run == True ):
# def main():

def abort():
  print("");
  import traceback;
  traceback.print_exc();
  input("Press <ENTER> to exit");# Pause to display 1st print message

def rol( val, shift_bits, max_bits ):
  rts = (val << shift_bits%max_bits) & (2**max_bits-1) | \
        ((val & (2**max_bits-1)) >> (max_bits-(shift_bits%max_bits)));
  return rts;

def ror( val, shift_bits, max_bits ):
  rts = ((val & (2**max_bits-1)) >> shift_bits%max_bits) | \
        (val << (max_bits-(shift_bits%max_bits)) & (2**max_bits-1))
  return rts;


##############################################################################
# process_payload() : Bridge between TCP Socket and Backdoor Poke
# process the Payload. It is a single simple ASCII backdoor command like:
# "w 00000000 aa55aa55" or "r 0000001c"
# Note: Also implements new Poke "loop read" command "k" for reading same
# address multiple times. This dramatically speeds up SUMP access as all data
# is read from single 32bit PCIe register.
# Note: Does not handle burst writes or reads. Keeping it simple to start out.
def process_payload( bd, payload ):
  line_list = payload.rstrip("\n").split("\n");
  rts = "";
  for each_line in line_list:
    cmd_list = each_line.split()+[None]*10;#"w 0 1" to a list of cmd,addr,data
    # 'w' : Write a single dword
    if ( cmd_list[0] == "w" ):
      addr = ( int( cmd_list[1], 16) );
      data_list = [];
      data_list_str = cmd_list[2:];
      for each in data_list_str[:]:
        if ( each != None ):
          data_list.append( int( each, 16 ));
      bd.wr( addr, data_list );

    # 'W' : Write multiple dwords to same address
    # Backdoor has a special command for this, PCIe just loops
    elif ( cmd_list[0] == "W" ):
      addr = ( int( cmd_list[1], 16) );
      data_list = [];
      data_list_str = cmd_list[2:];
      for each in data_list_str[:]:
        if ( each != None ):
          data_list.append( int( each, 16 ));
      if ( False ):
        for each in data_list:
          bd.wr( addr, [each] );
      else:
        bd.wr_repeat( addr, data_list );

    # Read one or many dwords
    elif ( cmd_list[0] == "r" ):
      addr = ( int( cmd_list[1], 16) );
      len = 0x0;# Single DWORD
      if ( cmd_list[2] != None ):
        if ( cmd_list[2] != "" ):
          len  = ( int( cmd_list[2], 16) );
      data_list = bd.rd( addr, (len+1) );
      rts = "";
      for each in data_list:
        rts += "%08x " % each;
      rts = rts.rstrip();

    # Read repeat a single dword address multiple times ( speeds up SUMP )
    # This is a Backdoor command, but not a PCIe command. PCIe just loops.
    elif ( cmd_list[0] == "k" ):
      addr = ( int( cmd_list[1], 16) );
      data = ( int( cmd_list[2], 16) );# How many times to read N-1
      if ( False ):
        rts = ""; k = 0;
        while ( k <= data ):
          data_list = bd.rd( addr, 0x1 );
#         rts += "%08x" % data_list[0];
          rts += "%08x " % data_list[0];# New 07_22_2016
          k = k + 1;
      else:
#       print("%08x %08x" % ( addr, data ) );
        data_list = bd.rd_repeat( addr, data );
        rts = "";
        for each in data_list:
#         rts += "%08x" % each;
#       rts = rts.rstrip();
          rts += "%08x " % each;# New 07_22_2016
      rts = rts.rstrip();

    # rt Register Test
    elif ( cmd_list[0] == "rt" ):
      addr = ( int( cmd_list[1], 16) );
      dur  = ( int( cmd_list[2], 16) );
      good_cnt = 0; wr_data = 0xF08155AA;
      old_data = bd.rd( addr, 0x1 )[0];
      for k in range( dur ):
        bd.wr( addr, [ wr_data ] );
        rd1_data = bd.rd( addr, 0x1 )[0];
        rd2_data = bd.rd( addr, 0x1 )[0];
        if ( rd1_data == wr_data & rd2_data == wr_data ):
          good_cnt +=1;
        # If Loop is small, display results to console
        if ( dur < 64 ):
          my_str = "".join([" %08x :" % addr] +    \
                      [" %08x :" % wr_data]  +  \
                      [" %08x" % rd1_data] +    \
                      [" %08x" % rd2_data]);
          print( my_str );
        wr_data = ror(wr_data,1,32);# Rotate Test Data 
      bd.wr( addr, [ old_data ] );# Put original value back in
      rts = "%08x" % good_cnt;

    # bs BitSet
    elif ( cmd_list[0] == "bs" ):
      addr = ( int( cmd_list[1], 16) );
      bit  = ( int( cmd_list[2], 16) );
      old_data = bd.rd( addr, 0x1 );
      new_data = old_data[0] | bit;
      bd.wr( addr, [new_data] );

    # bc BitClear
    elif ( cmd_list[0] == "bc" ):
      addr = ( int( cmd_list[1], 16) );
      bit  = ( int( cmd_list[2], 16) );
      old_data = bd.rd( addr, 0x1 );
      new_data = old_data[0] & ~bit;
      bd.wr( addr, [new_data] );
    
    # Configure the S3 Hubbard board FPGA over a FT232 USB link to cfg CPLD
    elif ( cmd_list[0] == "configure" ):
      if ( isinstance( bd, Backdoor_Poke_UART ) or 
           isinstance( bd, lb_link            )    ):
        file_name = cmd_list[1];
        bd.configure( file_name );

    # Accept MesaBus specific slot commands 
    elif ( cmd_list[0] == "mesa_slot" ):
      if ( isinstance( bd, Backdoor_Mesa_UART ) ):# Vs PCIe
        mesa_slot = int(cmd_list[1],16);
        print("mesa_slot = %02x" % mesa_slot );
        bd.slot = mesa_slot;

    # Accept MesaBus specific slot commands 
    elif ( cmd_list[0] == "mesa_subslot" ):
      if ( isinstance( bd, Backdoor_Mesa_UART ) ):# Vs PCIe
        mesa_subslot = int(cmd_list[1],16);
        print("mesa_subslot = %01x" % mesa_subslot );
        bd.subslot = mesa_subslot;

    # Send a BangBang to unlock poke hardware 
    elif ( cmd_list[0] == "!!" ):
      if ( isinstance( bd, Backdoor_Poke_UART ) ):# Vs PCIe
        bd.ser.flushOutput();
        bd.ser.flushInput();
        bd.ser.write( "!!".encode("utf-8"));

    # Nuke the socket from orbit. It's the only way to be sure.
    elif ( cmd_list[0] == "q" ):
      bd.close();
      s.close();
      read_list.remove(s);

  return rts;


###############################################################################
# vvvv New driver section supports both USB2 FT232 and USB3 FT600 links
#  lb_link <-> mesa_bus <-> usb_*_link 
#    * is either ft232 or ft600
###############################################################################


###############################################################################
# Protocol interface for MesaBus over a FTDI USB3 connection.
class lb_link:
  def __init__ ( self, mesa_bus, slot, subslot, debug ):
    self.mesa_bus  = mesa_bus;
    self.slot      = slot;
    self.subslot   = subslot;
    self.debug     = debug;
    self.phy_link  = mesa_bus.phy_link;

  def wr_repeat(self, addr, data_list ):
    self.wr( addr, data_list, repeat = True );

  def wr(self, addr, data_list, repeat = False ):
    # LocalBus WR cycle is a Addr+Data payload
    # Mesabus has maximum payload of 255 bytes, or 63 DWORDs.
    # 1 DWORD is LB Addr, leaving 62 DWORDs available for data bursts
    # if data is more than 62 dwords, parse it down into multiple bursts
    each_addr = addr;
    if ( repeat == False ):
      mb_cmd = 0x0;# Burst Write
    else:
      mb_cmd = 0x2;# Write Repeat ( Multiple data to same address - FIFO like )

    # Warning: Payloads greater than 29 can result with corruptions on FT600,
    # Example 0x11111111 becomes 0x1111111F.
    #    1 DWORD of MesaBus Header
    #    1 DWORD of LB Addr
    #   30 DWORD of LB Data
    #  ------
    #   32 DWORDs = 128 Binary Bytes = 256 ASCII Nibbles = 512 FT600 Bytes
    max_payload_len = 29;
    while ( len( data_list ) > 0 ):
      if ( len( data_list ) > max_payload_len ):
        data_payload = data_list[0:max_payload_len];
        data_list    = data_list[max_payload_len:];
      else:
        data_payload = data_list[0:];
        data_list    = [];
      payload = ( "%08x" % each_addr );
      for each_data in data_payload:
        payload += ( "%08x" % each_data );
        if ( repeat == False ):
          each_addr +=4;# maintain address for splitting into 62 DWORD bursts
      if ( self.debug ):
        print("LB.wr :" + payload );
      self.mesa_bus.wr( self.slot, self.subslot, mb_cmd, payload );
    return;

  def wr_packet(self, addr_data_list ):
    # FT600 has a 1024 byte limit. My 8bit interface halves that to 512 bytes
    # and send ASCII instead of binary, so 256
    max_packet_len = 30;
    while ( len( addr_data_list ) > 0 ):
      if ( len( addr_data_list ) > max_packet_len ):
        data_payload   = addr_data_list[0:max_packet_len];
        addr_data_list = addr_data_list[max_packet_len:];
      else:
        data_payload   = addr_data_list[0:];
        addr_data_list = [];
      payload = "";
      for each_data in data_payload:
        payload += ( "%08x" % each_data );
      mb_cmd = 0x4;# Write Packet
      if ( self.debug ):
        print("LB.wr :" + payload );
      self.mesa_bus.wr( self.slot, self.subslot, mb_cmd, payload );
    return;

  def rd_repeat(self, addr, num_dwords ):
    rts = self.rd( addr, num_dwords+1, repeat = True );
    return rts;

  def rd(self, addr, num_dwords, repeat = False ):
    max_payload = 31;
    if ( num_dwords <= max_payload ):
      rts = self.rd_raw( addr, num_dwords, repeat );
    else:
      # MesaBus has 63 DWORD payload limit, so split up into multiple reads
      dwords_remaining = num_dwords;
      rts = [];
      while( dwords_remaining > 0 ):
        if ( dwords_remaining <= max_payload ):
          rts += self.rd_raw( addr, dwords_remaining, repeat );
          dwords_remaining = 0;
        else:
          rts += self.rd_raw( addr, max_payload, repeat );
          dwords_remaining -= max_payload;
          if ( not repeat ):
            addr += max_payload*4;# Note Byte Addressing
    return rts;

  def rd_raw(self, addr, num_dwords, repeat = False ):
    dwords_remaining = num_dwords;
    each_addr = addr;
    if ( repeat == False ):
      mb_cmd = 0x1;# Normal Read
    else:
      mb_cmd = 0x3;# Read  Repeat ( Multiple data to same address )

    # LocalBus RD cycle is a Addr+Len 8byte payload to 0x00,0x0,0x1
    payload = ( "%08x" % each_addr ) + ( "%08x" % num_dwords );
    if ( self.debug ):
      print("MB.wr :" + payload );
    self.mesa_bus.wr( self.slot, self.subslot, mb_cmd, payload );
    rts_str = self.mesa_bus.rd( num_dwords = num_dwords );
    if ( self.debug ):
      print("LB.rd :" + rts_str );

    rts = [];
    if ( len( rts_str ) >= 8 ):
      while ( len( rts_str ) >= 8 ):
        rts_dword = rts_str[0:8];
        rts_str   = rts_str[8:];
        if ( self.debug ):
          print("MB.rd :" + rts_dword );
        try:
          rts += [ int( rts_dword, 16 ) ];
        except:
          addr_str = "%08x" % each_addr;
          print("ERROR: Invalid LocalBus Read >" +
                 addr_str + "< >" + rts_mesa + "< >" + rts_dword + "<");
          if ( self.debug ):
            sys.exit();
          rts += [ 0xdeadbeef ];
    else:
      print("ERROR: Invalid LocalBus Read >" + rts_str + "<");
      rts += [ 0xdeadbeef ];
    return rts;


  # This sends a locally referenced top.bit binary file to 
  # S3 Hubbard Board FPGA for configuration over existing FT232 phy_link
  def configure(self, file_name ):
    print("bd.configure() " + file_name);
#   try:
    if ( True ):
      if ( file_name[-2:] != "gz" ):
        file_in = open ( file_name, 'rb' );# Read in binary top.bit file
        while True:
          packet = file_in.read( 4096 );   # 4K at a time
          if packet:
            self.phy_link.wr( packet, binary = True  );# send to serial port
          else:
            break;
        file_in.close();
      else:
        import gzip;
        file_in = open( file_name , 'rb' );
        file_in_gz = gzip.GzipFile( fileobj= file_in,mode='rb' );
        while True:
          packet = file_in_gz.read( 4096 );   # 4K at a time
          if packet:
            self.phy_link.wr( packet, binary = True );# send to serial port
          else:
            break;
        file_in_gz.close();
        file_in.close();
#     self.phy_link.ser.flushOutput();
#     self.phy_link.ser.flushInput();
      self.phy_link.wr("\n\n\n\nFFFFFFFF\n", binary = False);# Autobaud+Unlock

#   except:
#     print("bd.configure() FAILED!");
    return;


###############################################################################
# Routines for Reading and Writing Payloads over MesaBus
# A payload is a series of bytes in hexadecimal string format. A typical use
# for MesaBus is to transport a higher level protocol like Local Bus for 32bit
# writes and reads. MesaBus is lower level and transports payloads to a
# specific device on a serial chained bus based on the Slot Number.
# More info at : https://blackmesalabs.wordpress.com/2016/03/04/mesa-bus/
#  0x0 : Write Cycle    : Payload of <ADDR><DATA>...
#  0x1 : Read  Cycle    : Payload of <ADDR><Length>
#  0x2 : Write Repeat   : Write burst data to single address : <ADDR><DATA>...
#  0x3 : Read  Repeat   : Read burst data from single address : <ADDR><Length>
#  0x4 : Write Multiple : Payload of <ADDR><DATA><ADDR><DATA><ADDR><DATA>..

class mesa_bus:
  def __init__ ( self, phy_link, lf, debug ):
    self.phy_link = phy_link;
# Note: type() doesn't work right in Python2, so tossed
#    if ( type( phy_link ) == usb_ft232_link ):
#      self.lf = "\n";
#    else:
#      self.lf = "";
    self.debug = debug;
    self.lf = lf;
    self.phy_link.wr( self.lf );
    self.phy_link.wr("FFFFFFFF" + self.lf );# HW releases Reset after 8 0xF

  def wr( self, slot, subslot, cmd, payload ):
#   preamble  = "F0";
    preamble  = "FFF0";
    slot      = "%02x" % slot;
    subslot   = "%01x" % subslot;
    cmd       = "%01x" % cmd;
    num_bytes = "%02x" % int( len( payload ) / 2 );
    mesa_str  = preamble + slot + subslot + cmd + num_bytes + payload+self.lf;
    if ( self.debug ):
      print( mesa_str );
    self.phy_link.wr( mesa_str );
    return;

  def rd( self, num_dwords ):
    #   "F0FE0004"+"12345678"
    #   "04" is num payload bytes and "12345678" is the read payload
    rts = self.phy_link.rd( bytes_to_read = (1+num_dwords)*4 );
    if ( self.debug ):
      print( rts );
    if ( len( rts ) > 8 ):
      rts = rts[8:];# Strip the "FOFE0004" header
    return rts;


###############################################################################
# Serial port class for sending and receiving ASCII strings to FT232RL UART
# Note - isn't ft232 specific, should work with any generic USB to UART chip
class usb_ft232_link:
  def __init__ ( self, port_name, baudrate, debug ):
    self.debug = debug;
    try:
      import serial;
    except:
      raise RuntimeError("ERROR: PySerial from sourceforge.net is required");
      raise RuntimeError(
         "ERROR: Unable to import serial\n"+
         "PySerial from sourceforge.net is required for Serial Port access.");
    try:
      self.ser = serial.Serial( port=port_name, baudrate=baudrate,
                               bytesize=8, parity='N', stopbits=1,
                               timeout=1, xonxoff=0, rtscts=0,dsrdtr=0);
      self.port = port_name;
      self.baud = baudrate;
      self.ser.flushOutput();
      self.ser.flushInput();
      self.ack_state = True;
    except:
      raise RuntimeError("ERROR: Unable to open USB COM Port "+port_name)

  def rd( self, bytes_to_read ):
    rts = self.ser.readline();
    if ( self.debug ):
      print("FT232_RD:"+rts);
    return rts;

  def wr( self, str, binary = False ):
    if ( binary ):
      self.ser.write( str );
    else:
      self.ser.write( str.encode("utf-8") );
      if ( self.debug ):
        print("FT232_WR:"+str);
    return;

  def close(self):
    self.ser.close();
    return;


###############################################################################
# class for sending and receiving ASCII strings to FTDI FT600 chip
# Note: Look at ftd3xx.py for list of functions in Python
class usb_ft600_link:
  def __init__ ( self, debug ):
    self.debug = debug;
    try:
      import ftd3xx
      import sys
      if sys.platform == 'win32':
        import ftd3xx._ftd3xx_win32 as _ft
      elif sys.platform == 'linux2':
        import ftd3xx._ftd3xx_linux as _ft
    except:
      raise RuntimeError("ERROR: FTD3XX from FTDIchip.com is required");
    try:
      # check connected devices
      numDevices = ftd3xx.createDeviceInfoList()
      if (numDevices == 0):
        print("ERROR: No FTD3XX device is detected.");
        return False;
      # devList = ftd3xx.getDeviceInfoList();
      devIndex = 0; # Assume a single device and open first device
      self.D3XX = ftd3xx.create(devIndex, _ft.FT_OPEN_BY_INDEX);

      if (self.D3XX is None):
        print("ERROR: Please check if another D3XX application is open!");
        return False;

      # Reset the FT600 to make sure starting fresh with nothing in FIFOs
      self.D3XX.resetDevicePort(); # Flush
      self.D3XX.close();
      self.D3XX = ftd3xx.create(devIndex, _ft.FT_OPEN_BY_INDEX);

      # check if USB3 or USB2
      devDesc = self.D3XX.getDeviceDescriptor();
      bUSB3 = devDesc.bcdUSB >= 0x300;

      # validate chip configuration
      cfg = self.D3XX.getChipConfiguration();

      # Timeout is in ms,0=Blocking. Defaults to 5,000
      rts = self.D3XX.setPipeTimeout( pipeid = 0xFF, timeoutMS = 1000 );

    # process loopback for all channels
    except:
      raise RuntimeError("ERROR: Unable to open USB Port " );
    return;

  def get_cfg( self ):
    cfg = self.D3XX.getChipConfiguration();
    return cfg;

  def set_cfg( self, cfg ):
    rts = self.D3XX.setChipConfiguration(cfg);
    return rts;

  def wr( self, str ):
    if ( self.debug ):
      print("FT600_WR:" +  str );
    str = "~".join( str );# only using 8bits of 16bit FT600, so pad with ~
    bytes_to_write = len( str );# str is now "~1~2~3 .. ~e~f" - Twice as long
    channel = 0;
    result = False;
    timeout = 5;
    tx_pipe = 0x02 + channel;
    if sys.platform == 'linux2':
      tx_pipe -= 0x02;
    if ( sys.version_info.major == 3 ):
      str = str.encode('latin1');
    xferd = 0
    while ( xferd != bytes_to_write ):
      # write data to specified pipe
      xferd += self.D3XX.writePipe(tx_pipe,str,bytes_to_write-xferd);
    return;

  def rd( self, bytes_to_read ):
    bytes_to_read = bytes_to_read * 4;# Only using 8 of 16bit of FT600, ASCII
    channel = 0;
    rx_pipe = 0x82 + channel;
    if sys.platform == 'linux2':
      rx_pipe -= 0x82;
    output = self.D3XX.readPipeEx( rx_pipe, bytes_to_read );
    xferd = output['bytesTransferred']
    if sys.version_info.major == 3:
      buff_read = output['bytes'].decode('latin1');
    else:
      buff_read = output['bytes'];

    while (xferd != bytes_to_read ):
      status = self.D3XX.getLastError()
      if (status != 0):
        print("ERROR READ %d (%s)" % (status,self.D3XX.getStrError(status)));
        if sys.platform == 'linux2':
          return self.D3XX.flushPipe(pipe);
        else:
          return self.D3XX.abortPipe(pipe);
      output = self.D3XX.readPipeEx( rx_pipe, bytes_to_read - xferd );
      status = self.D3XX.getLastError()
      xferd += output['bytesTransferred']
      if sys.version_info.major == 3:
        buff_read += output['bytes'].decode('latin1')
      else:
        buff_read += output['bytes']
    if ( self.debug ):
      print("FT600_RD:" +  buff_read[0::2] );
    return buff_read[0::2];# Return every other ch as using 8 of 16 FT600 bits

  def close(self):
    self.D3XX.resetDevicePort(); # Flush anything in chip
    self.D3XX.close();
    self.D3XX = 0;
    return;


###############################################################################
# vvvv Legacy pre-USB3 support stuff follows vvvv
###############################################################################


###############################################################################
# LEGACY
# Routines for Reading and Writing Payloads over MesaBus
# A payload is a series of bytes in hexadecimal string format. A typical use
# for MesaBus is to transport a higher level Local Bus protocol for 32bit
# writes and reads. MesaBus is lower level and transports payloads to a
# specific device on a serial chained bus based on the Slot Number.
# More info at : https://blackmesalabs.wordpress.com/2016/03/04/mesa-bus/
class legacy_mesa_bus:
  def __init__ ( self, port ):
    self.port = port;# See com_link.py
    self.port.wr("\n");# For autobaud

  def wr( self, slot, subslot, cmd, payload ):
#   preamble  = "\nFFF0";
    preamble  = "F0";
    slot      = "%02x" % slot;
    subslot   = "%01x" % subslot;
    cmd       = "%01x" % cmd;
    num_bytes = "%02x" % int( len( payload ) / 2 );
    mesa_str  = preamble + slot + subslot + cmd + num_bytes + payload + "\n";
    self.port.wr( mesa_str );
    return;

  def rd( self ):
    rts = self.port.rd();
    return rts;


###############################################################################
# LEGACY
# Serial port class for sending and receiving ASCII strings : MesaBus only
class com_link:
  def __init__ ( self, port_name, baudrate ):
    try:
      import serial;
    except:
      raise RuntimeError("ERROR: PySerial from sourceforge.net is required");
      raise RuntimeError(
         "ERROR: Unable to import serial\n"+
         "PySerial from sourceforge.net is required for Serial Port access.");
    try:
      self.ser = serial.Serial( port=port_name, baudrate=baudrate,
                               bytesize=8, parity='N', stopbits=1,
                               timeout=1, xonxoff=0, rtscts=0,dsrdtr=0);
      self.port = port_name;
      self.baud = baudrate;
      self.ser.flushOutput();
      self.ser.flushInput();
      self.ack_state = True;
    except:
      raise RuntimeError("ERROR: Unable to open USB COM Port "+port_name)

  def rd( self ):
    rts = self.ser.readline();
    return rts;

  def wr( self, str ):
    self.ser.write( str.encode("utf-8") );
    return;


###############################################################################
# LEGACY
# Protocol interface for MesaBus over a UART PySerial connection.
class Backdoor_Mesa_UART:
  def __init__ ( self, port_name, baudrate, slot, subslot ):
    # If 'AUTO' is specified, go with last COM listed with 'FTDIBUS'
    if ( port_name.upper() == "AUTO" ):
      import serial.tools.list_ports;
      port_list = serial.tools.list_ports.comports();
      # [('COM26','USB Serial Port (COM26)','FTDIBUS\\..'),
      #  ('COM4','USB Serial Port (COM4)','FTDIBUS\\...)]
      # This is really strange, when running from command line, FTDIBUS shows
      # up in the each[2] field. When double-clicking on bd_server.py it 
      # does not. Solution is to search for "USB Serial Port" instead which
      # appears to work well for both.
      found = None;
      for each in port_list:
        if ( "USB Serial Port" in each[1] ):
          found = each[0];
      port_name = found;
#         print("FOUND " + found );
#       print("----");
#       print( each );
#       print( each[0] );
#       print( each[1] );
#       print( each[2] );
#       if ( each[2] != 'n/a' ):
#         if ( "FTDIBUS" in each[2] ):
#           found = each[0];
#           print("FOUND " + found );

    self.com_link = com_link( port_name=port_name,baudrate=baudrate);
    self.mesa_bus = legacy_mesa_bus( self.com_link);# Establish MesaBus link
    self.slot      = slot;
    self.subslot   = subslot;
    self.dbg_flag  = False;
    self.port_name = port_name;

  def wr_repeat(self, addr, data):
    self.wr( addr, data, repeat = True );

  def wr(self, addr, data, repeat = False ):
    # LocalBus WR cycle is a Addr+Data 8byte payload
    # Mesabus has maximum payload of 255 bytes, or 63 DWORDs.
    # 1 DWORD is LB Addr, leaving 62 DWORDs available for data bursts
    # if data is more than 62 dwords, parse it into multiple bursts
    each_addr = addr;
    data_list = data;
    if ( repeat == False ):
      mb_cmd = 0x0;# Burst Write
    else:
      mb_cmd = 0x2;# Write Repeat ( Multiple data to same address )

    while ( len( data_list ) > 0 ):
      if ( len( data_list ) > 62 ):
        data_payload = data_list[0:62];
        data_list    = data_list[62:];
      else:
        data_payload = data_list[0:];
        data_list    = [];
      payload = ( "%08x" % each_addr );
      for each_data in data_payload:
        payload += ( "%08x" % each_data );
        if ( repeat == False ):
          each_addr +=4;
      self.mesa_bus.wr( self.slot, self.subslot, mb_cmd, payload );
    return;

  def rd_repeat( self, addr, num_dwords=1 ):
#   rts = self.rd( addr, num_dwords  , repeat = True );
    rts = self.rd( addr, num_dwords+1, repeat = True );
    return rts;

  def rd( self, addr, num_dwords=1, repeat = False ):
    dwords_remaining = num_dwords;
    each_addr = addr;
    if ( repeat == False ):
      mb_cmd = 0x1;# Normal Read 
    else:
      mb_cmd = 0x3;# Read  Repeat ( Multiple data to same address )
    rts = [];
    rts_dword = "00000000";
    while ( dwords_remaining > 0 ):
      if ( dwords_remaining > 62 ):
        n_dwords = 62;
        dwords_remaining -= 62;
      else:
        n_dwords = dwords_remaining;
        dwords_remaining = 0;

      # LocalBus RD cycle is a Addr+Len 8byte payload to 0x00,0x0,0x1
      payload = ( "%08x" % each_addr ) + ( "%08x" % n_dwords );
      self.mesa_bus.wr( self.slot, self.subslot, mb_cmd, payload );
      rts_mesa = self.mesa_bus.rd();
      # The Mesa Readback Ro packet resembles a Wi Write packet from slot 0xFE
      # This is to support a synchronous bus that clocks 0xFFs for idle
      # This only handles single DWORD reads and checks for:
      #   "F0FE0004"+"12345678" + "\n"
      #   "04" is num payload bytes and "12345678" is the read payload
      if ( len( rts_mesa ) > 8 ):
        rts_str = rts_mesa[8:];# Strip the FOFE0004 header
        while ( len( rts_str ) >= 8 ):
          rts_dword = rts_str[0:8];
          rts_str   = rts_str[8:];
          try:
            rts += [ int( rts_dword, 16 ) ];
          except:
            addr_str = "%08x" % each_addr;
            print("ERROR: Invalid LocalBus Read >" +
                   addr_str + "< >" + rts_mesa + "< >" + rts_dword + "<");
            if ( self.dbg_flag == "debug" ):
              sys.exit();
            rts += [ 0xdeadbeef ];
      else:
        print("ERROR: Invalid LocalBus Read >" + rts_mesa + "<");
        rts_mesa = self.mesa_bus.rd();
        print("ERROR2: Invalid LocalBus Read >" + rts_mesa + "<");
        if ( self.dbg_flag == "debug" ):
          sys.exit();
        rts += [ 0xdeadbeef ];
      if ( repeat == False ):
        each_addr += ( 4 * n_dwords );
    return rts;


  # This sends a locally referenced top.bit binary file to 
  # S3 Hubbard Board FPGA for configuration over Backdoor.
  def configure(self, file_name ):
    print("bd.configure() " + file_name);
    try:
      if ( file_name[-2:] != "gz" ):
        file_in = open ( file_name, 'rb' );# Read in binary top.bit file
        while True:
          packet = file_in.read( 4096 );   # 4K at a time
          if packet:
            self.com_link.write( packet );     # send out to serial port
          else:
            break;
        file_in.close();
      else:
        import gzip;
        file_in = open( file_name , 'rb' );
        file_in_gz = gzip.GzipFile( fileobj= file_in,mode='rb' );
        while True:
          packet = file_in_gz.read( 4096 );   # 4K at a time
          if packet:
            self.com_link.write( packet );     # send out to serial port
          else:
            break;
        file_in_gz.close();
        file_in.close();
      self.com_link.flushOutput();
      self.com_link.flushInput();
#     self.ser.write("!!");
#     rts = self.ser.readline();
      print("bd.configure() Complete");
    except:
      print("bd.configure() FAILED!");
    rts = "\n";
    return [rts];

  def close(self):
    self.com_link.flushOutput();
    self.com_link.flushInput();
    self.com_link.close()

  def __del__(self):
    try:
      self.com_link.close()
    except:
      raise RuntimeError("Backdoor ERROR: Unable to close COM Port!!")


###############################################################################
# Protocol interface for Poke over a UART PySerial connection.
# Note: Enabling posted_writes decreased write to write gaps from 16ms to 1ms
#       This feature doesn't wait for serial port <ACK> and instead just flushes
class Backdoor_Poke_UART:
  def __init__ ( self, port_name, baudrate, posted_writes ):
    try:
      import serial;
    except:
      raise RuntimeError("ERROR: PySerial from sourceforge.net is required");
      raise RuntimeError(         
         "ERROR: Unable to import serial\n"+
         "PySerial from sourceforge.net is required for USB connection.");
    try:
      self.ser = serial.Serial( port=port_name, baudrate=baudrate,
                               bytesize=8, parity='N', stopbits=1,
                               timeout=1, xonxoff=0, rtscts=0,     );
      self.port = port_name;
      self.baud = baudrate;
      self.posted_writes = posted_writes;
      self.ser.flushOutput();
      self.ser.flushInput();
      self.ser.write( "!!".encode("utf-8"));
      self.ser.write( "E 8\n".encode("utf-8") );
      self.ack_state = True;
      rts = self.ser.readline();

    except:
      raise RuntimeError("ERROR: Unable to open USB COM Port "+port_name)

  def wr(self, addr, data):
    if ( self.posted_writes == True and self.ack_state == True ):
      self.ser.write( "E 0\n".encode("utf-8") );
      self.ack_state = False;

    s = "".join(["w %x" % addr] +
                [" %x" % d for d in data] +
                ["\n"])
    self.ser.write(s.encode("utf-8"));
    if ( self.posted_writes == False ):
      rts = self.ser.readline();
      rts = rts.decode("utf-8");
      if '\n' not in rts:
        print("ERROR: bd.wr() : Serial Port Timeout!! " + s + ":" + rts );

  def wr_repeat(self, addr, data):
    if ( self.posted_writes == True and self.ack_state == True ):
      self.ser.write( "E 0\n".encode("utf-8") );
      self.ack_state = False;

    s = "".join(["W %x" % addr] +
                [" %x" % d for d in data] +
                ["\n"])
    self.ser.write(s.encode("utf-8"));
    if ( self.posted_writes == False ):
      rts = self.ser.readline();
      rts = rts.decode("utf-8");
      if '\n' not in rts:
        print("ERROR: bd.wr() : Serial Port Timeout!! " + s + ":" + rts );


  def rd( self, addr, num_dwords=1 ):
    if ( self.posted_writes == True and self.ack_state == False ):
      self.ser.write( "E 8\n".encode("utf-8") );
      rts = self.ser.readline();
      self.ack_state = True;

    s = "r %x %x\n" % (addr, (num_dwords-1)); # Non-RLE Read
    self.ser.write(s.encode("utf-8"));
    rts = self.ser.readline();
    rts = rts.decode("utf-8");
    try:
      rts_conv = [int(rts[8*k:8*(k + 1)], 16) for k in range(num_dwords)]
    except:
      print("ERROR: bd.rd() Serial Port Timeout!!");
      rts_conv = [0xdeaddead];
    return rts_conv;

  def rd_repeat( self, addr, num_dwords=1 ):
    if ( self.posted_writes == True and self.ack_state == False ):
      self.ser.write( "E 8\n".encode("utf-8") );
      rts = self.ser.readline();
      self.ack_state = True;
    # Sump has already subtracted 1 from num_dwords
    s = "k %x %x\n" % (addr, (num_dwords) ); #  Loop Read
    self.ser.write(s.encode("utf-8"));
    rts = self.ser.readline();
    rts = rts.decode("utf-8");
    foo = len( rts );
    try:
      rts_conv = [int(rts[8*k:8*(k + 1)], 16) for k in range(num_dwords+1)]
    except:
      print("ERROR: bd.rd_repeat() Serial Port Timeout!!");
      rts_conv = [0xdeaddead];
    return rts_conv;

  # This sends a locally referenced top.bit binary file to 
  # S3 Hubbard Board FPGA for configuration over Backdoor.
  def configure(self, file_name ):
    print("bd.configure() " + file_name);
    try:
      if ( file_name[-2:] != "gz" ):
        file_in = open ( file_name, 'rb' );# Read in binary top.bit file
        while True:
          packet = file_in.read( 4096 );   # 4K at a time
          if packet:
            self.ser.write( packet );      # send out to serial port
          else:
            break;
        file_in.close();
      else:
        import gzip;
        file_in = open( file_name , 'rb' );
        file_in_gz = gzip.GzipFile( fileobj= file_in,mode='rb' );
        while True:
          packet = file_in_gz.read( 4096 );# 4K at a time
          if packet:
            self.ser.write( packet );      # send out to serial port
          else:
            break;
        file_in_gz.close();
        file_in.close();
      self.ser.flushOutput();
      self.ser.flushInput();
      self.ser.write("!!");
      rts = self.ser.readline();
      print("bd.configure() Complete");
    except:
      print("bd.configure() FAILED!");
    rts = "\n";
    return [rts];

  def close(self):
    if ( self.posted_writes == True and self.ack_state == False ):
      self.ser.write( "E 8\n".encode("utf-8") );
      rts = self.ser.readline();
      self.ack_state = True;
    self.ser.flushOutput();
    self.ser.flushInput();
    self.ser.close()

  def __del__(self):
    try:
      self.ser.close()
    except:
      raise RuntimeError("Backdoor ERROR: Unable to close COM Port!!")


try:
  if __name__=='__main__': main()
except KeyboardInterrupt:
  print('Break!')
# EOF