https://blackmesalabs.wordpress.com/2016/12/22/sump2-100-msps-32bit-logic-analyzer...
[BML_sump2] / sump2.py
index 86478af..796f113 100755 (executable)
--- a/sump2.py
+++ b/sump2.py
@@ -2,7 +2,6 @@
 # sump2
 #               Copyright (c) Kevin M. Hubbard 2016 BlackMesaLabs
 #
-#
 # 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 2 of the License, or (at your option) any later 
 #   https://pypi.python.org/packages/...../pyserial-3.1.1-py2.py3-none-any.whl
 #   pip install pyserial-3.1.1-py2.py3-none-any.whl
 #
+# [ spidev for RaspberryPi SPI Communications to icoboard ]
+#   sudo apt-get install python-dev
+#   sudo apt-get install git
+#   git clone git://github.com/doceme/py-spidev
+#   cd py-spidev
+#   sudo python setup.py install
+#
 # TODO: Vertical resizing of window has issues. Signal scrolling isnt updated
 #
 # Revision History:
 #       10.20.16 khubbard   Improve centering to trigger post acquisition. 
 #       10.21.16 khubbard   Acquisition_Length fixed. Also works with RLE now.
 #       10.24.16 khubbard   Fixed RLE cropping not showing DWORDs.Speed Improvs
+#       12.19.16 khubbard   Fork for Pi and icoboard from Windows original.
 ###############################################################################
 import time   
 from time import sleep;
@@ -113,13 +120,13 @@ import sys;
 import os;
 import platform;
 import locale;
-
+import spidev;
 
 class main(object):
  def __init__(self):
 # import math   # pow
 # import types  # type
-  self.vers = "2016.10.24";
+  self.vers = "2016.12.21";
   print("Welcome to SUMP2 " + self.vers + " by BlackMesaLabs");
   self.mode_cli = True;
 
@@ -135,7 +142,7 @@ class main(object):
   self.math   = math;
   list2file( self, "sump2_manual.txt", init_manual(self ) );
 
-  locale.setlocale( locale.LC_NUMERIC, 'English' );
+# locale.setlocale( locale.LC_NUMERIC, 'English' );
 
   init_globals( self );# Internal software variables
   self.file_log = open ( self.vars["file_log"] , 'w' );
@@ -226,6 +233,7 @@ class main(object):
 
     # Load in the wavefile
     if os.path.exists( file_name ):
+      draw_header( self, ( "load_format() "+ file_name ) );# Pi
       print( "load_format() ", file_name );
       load_format( self, file_name ); 
       trig_i = sump_dump_data(self);
@@ -272,11 +280,12 @@ class main(object):
 
   recalc_max_samples( self );
 
-
   # Draw the 1st startup screen
   screen_refresh( self );
   self.context = "gui";
 
+  self.pygame.mouse.set_cursor(*pygame.cursors.arrow);# Pi
+
   # GUI Main Loop
   self.clock = self.pygame.time.Clock();
   self.time  = self.pygame.time;
@@ -326,8 +335,12 @@ class main(object):
           # for non-RLE this is trivial, for RLE it is more complicated
           trig_to_start = trig_i - 0;
           trig_to_end   = self.max_samples - trig_i;
-          start = trig_i - min( trig_to_start, trig_to_end );
-          stop  = trig_i + min( trig_to_start, trig_to_end );
+#         start = trig_i - min( trig_to_start, trig_to_end );
+#         stop  = trig_i + min( trig_to_start, trig_to_end );
+# HERE88
+# Pi is slower for rendering, so render less
+          start = trig_i - min( trig_to_start, trig_to_end ) // 2;
+          stop  = trig_i + min( trig_to_start, trig_to_end ) // 2;
 
           proc_cmd( self, "zoom_to", [str(start), str(stop) ] );
 #         proc_cmd( self, "zoom_to", ["0", str( self.max_samples ) ] );
@@ -345,12 +358,12 @@ class main(object):
 
     for event in pygame.event.get(): # User did something
       # VIDEORESIZE
-      if event.type == pygame.VIDEORESIZE:
-        self.screen= pygame.display.set_mode(event.dict['size'], 
-                            pygame.RESIZABLE | 
-                            pygame.HWSURFACE |
-                            pygame.DOUBLEBUF);
-        self.resize_on_mouse_motion = True;# Delay redraw until resize done
+#     if event.type == pygame.VIDEORESIZE:
+#       self.screen= pygame.display.set_mode(event.dict['size'], 
+#                           pygame.RESIZABLE | 
+#                           pygame.HWSURFACE |
+#                           pygame.DOUBLEBUF);
+#       self.resize_on_mouse_motion = True;# Delay redraw until resize done
 
       # Detect when console box has gained focus and switch from GUI to BD_SHELL
       # and loop in a keyboard loop processing commands. Exit on a NULL Command.
@@ -376,6 +389,8 @@ class main(object):
           proc_cmd( self, "font_larger"  , [] );
         elif ( event.key == pygame.K_END       ):
           proc_cmd( self, "font_smaller" , [] );
+        elif ( event.key == pygame.K_Q         ):
+          proc_cmd( self, "quit" , [] );
         elif ( event.key == pygame.K_RIGHT ):
           num_samples = self.sample_room // 16;
           proc_cmd( self, "scroll_right", [str(num_samples)] );
@@ -660,7 +675,7 @@ class main(object):
 #   draw_screen( self );
 #   screen_flip( self );
 
-  shutdown( self );
+  shutdown( self, False, False );
   return;# This is end of main program loop
 
 ###############################################################################
@@ -690,12 +705,28 @@ def display_init( self ):
   log( self, ["display_init()"] );
   import pygame # Import PyGame Module
   pygame.init() # Initialize the game engine
-  self.screen_width  = int( self.vars["screen_width"], 10 );
-  self.screen_height = int( self.vars["screen_height"], 10 );
+# self.screen_width  = int( self.vars["screen_width"], 10 );
+# self.screen_height = int( self.vars["screen_height"], 10 );
   # pygame.NOFRAME, pygame.FULLSCREEN
+
+  ( self.screen_width, self.screen_height ) = \
+    (pygame.display.Info().current_w, pygame.display.Info().current_h);
+  print( self.screen_width );
+  print( self.screen_height );
+
+# self.screen_width  = 800;
+# self.screen_height = 600;
+
   self.screen=pygame.display.set_mode(
                     [ self.screen_width, self.screen_height ],
-                    pygame.RESIZABLE | pygame.HWSURFACE | pygame.DOUBLEBUF );
+                    pygame.FULLSCREEN );
+#                   pygame.FULLSCREEN | pygame.HWSURFACE | pygame.DOUBLEBUF );
+
+# self.screen=pygame.display.set_mode(
+#                   [ self.screen_width, self.screen_height ],
+#                   pygame.RESIZABLE | pygame.HWSURFACE | pygame.DOUBLEBUF );
+
+
   self.pygame = pygame;
   self.pygame.display.set_icon( create_icon( self ) );
   draw_header( self, "" );
@@ -1322,7 +1353,8 @@ def init_vars( self, file_ini ):
   vars["bd_server_ip"          ] = "localhost";
   vars["bd_server_socket"      ] = "21567";
   vars["uut_name"              ] = "UUT";
-  vars["sump_addr"             ] = "00000090" ;# Addr of sump2_ctrl_reg
+# vars["sump_addr"             ] = "00000090" ;# Addr of sump2_ctrl_reg
+  vars["sump_addr"             ] = "00000010" ;# Addr of sump2_ctrl_reg
   vars["sump_script_inc_filter"] = "*.txt";
   vars["sump_script_exc_filter"] = "sump2_*.txt";
   vars["sump_trigger_type"     ] = "or_rising";
@@ -1725,9 +1757,18 @@ def proc_cmd( self, cmd, parms ):
   elif ( cmd == "bd_shell" ):
     bd_shell(self, cmd_start ="" );
 
-  elif ( cmd == "quit" or cmd == "exit" ):
+  elif ( cmd == "load_fpga" ):
+    load_fpga(self);
+
+  elif ( cmd=="quit" or cmd=="exit" or cmd=="shutdown" or cmd=="reboot" ):
     self.done=True;
-    shutdown( self );
+    if ( cmd == "quit" or cmd == "exit" ):
+      shutdown( self, False,False );# Normal App exit to command line
+    else:
+      if ( cmd == "reboot" ):
+        shutdown( self, True, True  );# Pi System Shutdown with Reboot
+      else:
+        shutdown( self, True, False );# Pi System Shutdown
 
   elif ( cmd == "sump_connect" ):
     sump_connect(self);
@@ -1985,6 +2026,7 @@ def proc_cmd( self, cmd, parms ):
     draw_header( self,"save_bmp() : Saved " + filename );
     self.last_filesave = filename;
   elif ( cmd == "save_png" ):
+    # Warning - on Pi red and green are swapped on PNG saves
     screen_erase( self );
     draw_screen( self );
     screen_flip( self );
@@ -1993,6 +2035,31 @@ def proc_cmd( self, cmd, parms ):
     draw_header( self,"save_png() : Saved " + filename );
     self.last_filesave = filename;
 
+#   # Workaround for Pi Pygame bug swapping Red and Green on PNG save
+#   # This requires PIL - not part of PyGame or Raspbian
+#   filename = make_unique_filename( self, "sump2_", ".bmp" );
+#   self.pygame.image.save( self.screen, filename );
+#   from PIL import Image;
+#   im = Image.open( filename );
+#   filename = make_unique_filename( self, "sump2_", ".png" );
+#   im.save( filename ) ;
+#   draw_header( self,"save_png() : Saved " + filename );
+#   self.last_filesave = filename;
+
+    # Workaround for Pi Pygame bug swapping Red and Green on PNG save
+    # This works, but takes a minute on just a 640x480 display
+#   img = self.pygame.surfarray.array3d( self.screen )
+#   img_copy = self.pygame.surfarray.array3d( self.screen )
+#   for y in range(0, self.screen_height ):
+#     for x in range(0, self.screen_width ):
+#       img_copy[x][y][0] = img[x][y][1];
+#       img_copy[x][y][1] = img[x][y][0];
+#   surf = self.pygame.surfarray.make_surface(img_copy);
+#   filename = make_unique_filename( self, "sump2_", ".png" );
+#   self.pygame.image.save( surf , filename );
+#   draw_header( self,"save_png() : Saved " + filename );
+#   self.last_filesave = filename;
+
   elif ( cmd == "font_larger" or cmd == "font_smaller" ):
     size = int( self.vars["font_size"] );
     if ( cmd == "font_larger" ):
@@ -2850,8 +2917,18 @@ def draw_header( self, txt ):
   if ( self.fatal_msg != None ):
     uut_name = "DEMO Mode :";
     txt = self.fatal_msg;
-  self.pygame.display.set_caption( \
-       "SUMP2 " + self.vers + " (c) 2016 BlackMesaLabs : "+uut_name+" "+txt);
+  msg ="SUMP2 " + self.vers + " (c) 2016 BlackMesaLabs : "+uut_name+" "+txt;
+  self.pygame.display.set_caption( msg );
+
+  # Pi display header at top of surface since full screen
+  msg += "                    ";# Erase old text that was wider
+  txt = self.font.render( msg , True, self.color_fg, self.color_bg );
+  y1 = self.txt_height // 2; # Gap from top border
+  x1 = self.txt_width;    # Gap from left border
+  self.screen.blit(txt, (x1,y1 ));
+  screen_flip( self );
+
+  #HERE99
   if ( self.gui_active == True ):
     import pygame;
     pygame.event.get();# Avoid "( Not Responding )"
@@ -3070,41 +3147,10 @@ def get_popup_sel( self ):
 # Find a monospaced font to use
 def get_font( self , font_name, font_height ):
   log( self, ["get_font() " + font_name ] );
-# print "get_font()";
-  import fnmatch;
-# font_name = "khmerossystem";
-# font_name = "dejavusansmono";
-  font_height = int( font_height, 10 ); # Conv String to Int
-  font_list = self.pygame.font.get_fonts(); # List of all fonts on System
-  self.font_list = [];
-  for each in font_list:
-    log( self, ["get_font() : Located Font = " + each ] );
-    # Make a list of fonts that might work based on their name
-    if ( ( "mono"    in each.lower()    ) or
-         ( "courier" in each.lower()    ) or
-         ( "fixed"   in each.lower()    )    ): 
-      self.font_list.append( each );
-  if ( font_name == None or font_name == "" ):
-    font_list = self.pygame.font.get_fonts(); # List of all fonts on System
-    for each in font_list:
-      log( self, ["get_font() : Located Font = " + each ] );
-    ends_with_mono_list = fnmatch.filter(font_list,"*mono");
-    if  ends_with_mono_list :
-      font_name = ends_with_mono_list[0];# Take 1st one
-#     log( self, ["get_font() : Using   Font = " + font_name ] );
-    else:
-      font_name = self.font_list[0]; # Take 1st one
-#     log( self, ["get_font() : Using   Font = " + font_name ] );
-  try:
-    font = self.pygame.font.SysFont( font_name , font_height );
-#   log( self, ["get_font() : Using   Font = " + font_name ] );
-  except:
-    font = self.pygame.font.Font( None , font_height );# Default Pygame Font
-#   log( self, ["get_font() : Using Default Font"] );
-
-  # Calculate Width and Height of font for future reference
-# txt = font.render("X",True, ( 255,255,255 ) );
+  # Pi Specific
+# font_size = 18;
+  font_size = int(font_height);
+  font = self.pygame.font.Font('freesansbold.ttf', font_size );
   txt = font.render("4",True, ( 255,255,255 ) );
   self.txt_width  = txt.get_width();
   self.txt_height = txt.get_height();
@@ -3167,7 +3213,8 @@ def draw_screen( self ):
 
   # 1st Display the Net Names
 # y = self.txt_height / 2; # Gap from top border
-  y = self.txt_height // 2; # Gap from top border
+# y = self.txt_height // 2; # Gap from top border
+  y = self.txt_height * 2; # Pi top header
   x = self.txt_width;     # Gap from left border
   self.sig_name_start_x = x;
   self.sig_name_start_y = y;
@@ -3361,6 +3408,8 @@ def draw_screen( self ):
     None;
   else:
     # print("Rendering samples.");
+    import time;
+    start_time = time.time();
     surface.fill( self.color_bg );
     if ( self.debug ):
       print( "value_surface_valid==False");
@@ -3557,6 +3606,9 @@ def draw_screen( self ):
       txt = "Fast Rendering Complete"; 
     else:
       txt = "Full Rendering Complete"; 
+    stop_time = time.time();
+    total_time = stop_time - start_time;
+    print("Render Time = %d" % total_time );
     draw_header( self, txt );
      
   x = self.sig_value_start_x;
@@ -3702,7 +3754,7 @@ def draw_screen( self ):
 # self.screen.blit(txt1, ( x, y1 ) );
 # self.screen.blit(txt2, ( x, y2 ) );
 # self.screen.blit(txt3, ( x, y2 ) );
-# print (str(self.max_samples));# HERE13
+# print (str(self.max_samples));# 
 
   y = self.screen_height - int(self.txt_height * 1.5 );
   x = self.sig_name_start_x;
@@ -4288,14 +4340,19 @@ def list_remove( my_list, item ):
 # Establish connection to Sump2 hardware
 def sump_connect( self ):
   log( self, ["sump_connect()"] );
-  self.bd=Backdoor(  self.vars["bd_server_ip"],
-                     int( self.vars["bd_server_socket"], 10 ) );# Note dec
-  if ( self.bd.sock == None ):
-    txt = "ERROR: Unable to locate BD_SERVER";
-    self.fatal_msg = txt;
-    print( txt );
-    log( self, [ txt ] );
-    return False;
+# self.bd=Backdoor(  self.vars["bd_server_ip"],
+#                    int( self.vars["bd_server_socket"], 10 ) );# Note dec
+# if ( self.bd.sock == None ):
+#   txt = "ERROR: Unable to locate BD_SERVER";
+#   self.fatal_msg = txt;
+#   print( txt );
+#   log( self, [ txt ] );
+#   return False;
+  self.spi_port = spidev.SpiDev();
+  self.spi_port.open(0,1);# Note: icoboard uses CE0 for Mach, CE1 for Ice
+  self.spi_port.max_speed_hz = 32000000;# 1-32 MHz typical
+  self.mb = mesa_bus( self.spi_port);# Establish a MesaBus link
+  self.bd = local_bus( self.mb, False );# Establish a LocalBus link
   
   self.sump = Sump2( self.bd, int( self.vars["sump_addr"],16 ) );
   self.sump.rd_cfg();# populate sump.cfg_dict[] with HW Configuration
@@ -4540,7 +4597,7 @@ def sump_dump_data( self ):
   events = ram_bytes * 8;# Example, 32 events total for 4 ram_bytes
   self.dwords_start = 0;
   self.dwords_stop  = ram_phys;
-
+  start_time = time.time();
   # Event Signals
   rd_page = 0;
   dump_data = sump_dump_var_ram(self,rd_page = rd_page );
@@ -4587,6 +4644,9 @@ def sump_dump_data( self ):
       my_signal.bit_bot   = 0;
       for j in range( 0, ram_len, 1):
         my_signal.values.append( "%08x" % dump_data[j] );
+  stop_time = time.time();
+  total_time = stop_time - start_time;
+  print("Download Time = %d" % total_time );
 
   sump_bundle_data( self );
   recalc_max_samples( self );
@@ -4693,6 +4753,7 @@ def sump_dump_rle_data( self ):
   list2file( self, "sump2_rle_dump.txt", rle_hex_list );
 
   print("expand_rle()");
+  start_time = time.time();
   (dump_data,trig_i) = expand_rle( self, start_t,stop_t,pre_trig,post_trig );
 # print( len( dump_data ) );
 
@@ -4807,6 +4868,9 @@ def sump_dump_rle_data( self ):
  
   sump_bundle_data( self );
   recalc_max_samples( self );
+  stop_time = time.time();
+  total_time = stop_time - start_time;
+  print("RLE Decompress Time = %d" % total_time );
   return trig_i;
 
 def rle_undersample_signal( self, undersample_rate, my_signal ):
@@ -5473,15 +5537,32 @@ def vcdfile2signal_list( self, file_name ):
   draw_header( self, "" );
   return;
 
-def shutdown( self ):
+
+def load_fpga( self ):
+  log( self, ["load_fpga()"]);
+  draw_header( self, "load_fpga()" );
+  os.system("sudo ./icoprog -f < top_bitmap.bin");# Program IcoBoard Flash
+  os.system("sudo ./icoprog -b");
+  draw_header( self, "Please reboot and restart SUMP2.py application" );
+
+
+def shutdown( self, system_shutdown = False, system_reboot = False ):
   log( self, ["shutdown()"]);
   var_dump( self, "sump2.ini" ); # Dump all variable to INI file
   proc_cmd( self, "save_format", [""] ); # Autosave the last format
+  if ( system_shutdown == True and system_reboot == False ):
+    draw_header( self, "Linux Shutdown initiated" );
+    os.system("sudo shutdown -h now");# Pi Shutdown ( Halt )
+  if ( system_shutdown == True and system_reboot == True  ):
+    draw_header( self, "Linux Reboot initiated" );
+    os.system("sudo shutdown -r now");# Pi Reboot
+
   if ( self.mode_cli == False ):
     self.pygame.quit();# Be IDLE friendly
   print("");
   print("Thank you for using SUMP2 " + self.vers + " by BlackMesaLabs");
   print("Please encourage the development and use of open-source software");
+
   sys.exit();
   return;
 
@@ -5695,7 +5776,11 @@ def init_globals( self ):
                 "Save_TXT","Save_VCD","Save_Rename"],
 #              ["Fonts","Font_Larger","Font_Smaller"],
                ["Misc","Font_Larger","Font_Smaller",
-               "BD_SHELL","Manual"],"Quit"];
+               "BD_SHELL","Manual"],
+#              "Quit"];
+               ["System","Quit","Shutdown","Reboot","Load_FPGA"]];# Pi
+
+
   self.popup_list = self.popup_list_values;
   self.cmd_alias_hash_dict = {};
   self.cmd_alias_hash_dict["zi"]    = "zoom_in";
@@ -5774,6 +5859,8 @@ class signal(object):
 class Sump2:
   def __init__ ( self, backdoor, addr ):
     self.bd = backdoor;
+
+
     self.addr_ctrl = addr;
     self.addr_data = addr + 0x4;
     self.cmd_state_idle        = 0x00;
@@ -6087,6 +6174,144 @@ class Backdoor:
       payload += bin_data.decode("utf-8");# ByteArray to String
     return payload;
 
+
 ###############################################################################
-main = main();
+# Routines for Reading and Writing a remote 32bit LocalBus over MesaBus
+# A local bus cycle is a pre-determined payload transported over the MesaBus
+# LocalBus is mapped to SubSlot 0x0
+#  0x0 : Write DWORD or Burst starting at Address
+#  0x1 : Read  DWORD or Burst starting at Address
+#  0x2 : Write Multiple DWORDs to same Address
+#  0x3 : Read Multiple DWORDs from same Address
+class local_bus:
+  def __init__ ( self, mesa_bus, dbg_flag ):
+    self.mesa_bus = mesa_bus;
+    self.dbg_flag = dbg_flag;
+
+  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;
+    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    = [];
+      # Note: MesaBus on icoboard doesn't support bursts writes yet.
+      if ( len( data_list ) > 1 ):
+        data_payload = data_list[0:1];
+        data_list    = data_list[1:];
+      else:
+        data_payload = data_list[0:];
+        data_list    = [];
+
+      payload = ( "%08x" % each_addr );
+      for each_data in data_payload:
+        payload += ( "%08x" % each_data );
+        each_addr +=4;
+#     print(payload);
+      self.mesa_bus.wr( 0x00, 0x0, 0x0, payload );
+    return;
+
+
+  def rd( self, addr, num_dwords = 1, repeat = False ):
+    dwords_remaining = num_dwords;
+    each_addr = addr;
+    rts = [];
+    rts_dword = "00000000";
+    while ( dwords_remaining > 0 ):
+      # MesaBus has 255 byte limit on payload, so split up large reads
+#     if ( dwords_remaining > 62 ):
+#       n_dwords = 62;
+#       dwords_remaining -= 62;
+#     else:
+#       n_dwords = dwords_remaining;
+#       dwords_remaining = 0;
+
+      # Note: MesaBus on icoboard doesn't support bursts or repeat reads yet.
+      if ( dwords_remaining > 1 ):
+        n_dwords = 1;
+        dwords_remaining -= 1;
+      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( 0x00, 0x0, 0x1, 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:
+#           print("ERROR:Invalid LocalBus Read "+rts_mesa+" "+rts_dword);
+            if ( self.dbg_flag == "debug" ):
+              sys.exit();
+            rts += [ 0x00000000 ];
+      else:
+#       print("ERROR: Invalid LocalBus Read " + rts_mesa + " " + rts_dword);
+        if ( self.dbg_flag == "debug" ):
+          sys.exit();
+        rts += [ 0x00000000 ];
+      if ( repeat == False ):
+        each_addr += ( 4 * n_dwords );# Note dword to byte addr translation
+#   print( rts );
+    return rts;
+
+
+###############################################################################
+# 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 mesa_bus:
+  def __init__ ( self, port ):
+    self.port = port;
+
+  def wr( self, slot, subslot, cmd, payload ):
+    preamble  = "FFF0";
+    slot      = "%02x" % slot;
+    subslot   = "%01x" % subslot;
+    cmd       = "%01x" % cmd;
+    num_bytes = "%02x" % ( len( payload ) / 2 );
+    mesa_str  = preamble + slot + subslot + cmd + num_bytes + payload;
+    if ( type( self.port ) == spidev.SpiDev ):
+      mesa_hex_list = [];
+      for i in range( 0, len(mesa_str)/2 ):
+        mesa_hex = int(mesa_str[i*2:i*2+2],16);
+        mesa_hex_list += [mesa_hex];
+      rts = self.port.xfer2( mesa_hex_list );
+    else:
+      self.port.wr( mesa_str );
+    return;
 
+  def rd( self ):
+    if ( type( self.port ) == spidev.SpiDev ):
+      hex_str = "";
+      rts = self.port.xfer2(8*[0xFF]);
+      for each in rts:
+        hex_str += ("%02x" % each );
+      rts = hex_str;
+    else:
+      rts = self.port.rd();
+    return rts;
+
+
+###############################################################################
+main = main();