
VERSION = '1.20'
title ="Arduboy FX data uploader v" + VERSION + " by Mr.Blinky Feb.2022-Mar.2023"

import sys
import os
import time

try: 
  toolspath = os.path.dirname(os.path.abspath(sys.argv[0]))
  sys.path.insert(0, toolspath)
  from serial.tools.list_ports  import comports
  from serial import Serial
except:
  sys.stderr.write("pySerial python module not found or wrong version.\n")
  sys.stderr.write("Make sure the correct module is installed or placed at {}\n".format(toolspath))
  sys.exit(-1)
  
compatibledevices = [
 #Arduboy Leonardo
 "VID:PID=2341:0036", "VID:PID=2341:8036",
 "VID:PID=2A03:0036", "VID:PID=2A03:8036",
 #Arduboy Micro
 "VID:PID=2341:0037", "VID:PID=2341:8037",
 "VID:PID=2A03:0037", "VID:PID=2A03:8037",
 #Genuino Micro
 "VID:PID=2341:0237", "VID:PID=2341:8237",
 #Sparkfun Pro Micro 5V
 "VID:PID=1B4F:9205", "VID:PID=1B4F:9206",
 #Adafruit ItsyBitsy 5V
 "VID:PID=239A:000E", "VID:PID=239A:800E",
]

manufacturers = {
  0x01 : "Spansion",
  0x14 : "Cypress",
  0x1C : "EON",
  0x1F : "Adesto(Atmel)",
  0x20 : "Micron",
  0x37 : "AMIC",
  0x9D : "ISSI",
  0xC2 : "General Plus",
  0xC8 : "Giga Device",
  0xBF : "Microchip",
  0xEF : "Winbond"
}

PAGESIZE = 256
BLOCKSIZE = 65536
PAGES_PER_BLOCK = BLOCKSIZE // PAGESIZE
MAX_PAGES = 65536
bootloader_active = False

def print(s):
  sys.stdout.write(s + '\n')
  sys.stdout.flush()

def getComPort(verbose):
  global bootloader_active
  devicelist = list(comports())
  for device in devicelist:
    for vidpid in compatibledevices:
      if  vidpid in device[2]:
        port=device[0]
        bootloader_active = (compatibledevices.index(vidpid) & 1) == 0
        if verbose : sys.stdout.write("Found {} at port {} ".format(device[1],port))
        return port
  if verbose : print("Arduboy not found.")

def bootloaderStart():
  global bootloader
  ## find and connect to Arduboy in bootloader mode ##
  port = getComPort(True)
  if port is None : sys.exit(-1)
  if not bootloader_active:
    print("Selecting bootloader mode...")
    try:
      bootloader = Serial(port,1200)
      time.sleep(0.1)
      bootloader.close()
      time.sleep(0.5)
    except:
        sys.stderr.write("COM port not available.\n")
        sys.exit(-1)
    #wait for disconnect and reconnect in bootloader mode
    while getComPort(False) == port :
      time.sleep(0.1)
      if bootloader_active: break        
    while getComPort(False) is None : time.sleep(0.1)
    port = getComPort(True)

  sys.stdout.write("Opening port ...")
  sys.stdout.flush()
  for retries in range(20):
    try:
      time.sleep(0.1)  
      bootloader = Serial(port,57600)
      break
    except:
      if retries == 19:
        print(" Failed!")
        sys.exit(-1)
      sys.stdout.write(".")
      sys.stdout.flush()
      time.sleep(0.4)
  print("\r")
  
def getVersion():
  bootloader.write(b"V")
  return int(bootloader.read(2))

def getJedecID():
  bootloader.write(b"j")
  jedec_id = bootloader.read(3)
  time.sleep(0.5)  
  bootloader.write(b"j")
  jedec_id2 = bootloader.read(3)
  if jedec_id2 != jedec_id or jedec_id == b'\x00\x00\x00' or jedec_id == b'\xFF\xFF\xFF':
    sys.stderr.write("No FX flash chip detected.\n")
    sys.exit(-1)
  return bytearray(jedec_id)
  
def bootloaderExit():
  global bootloader
  bootloader.write(b"E")
  bootloader.read(1)

################################################################################

def writeFlash(pagenumber, flashdata):
  bootloaderStart()
  
  #check version
  if getVersion() < 13:
    sys.stderr.write("Bootloader has no flash cart support\nWrite aborted!\n")
    sys.exit(-1)
  
  ## detect flash cart ##
  jedec_id = getJedecID()
  if jedec_id[0] in manufacturers.keys():
    manufacturer = manufacturers[jedec_id[0]]
  else:
    manufacturer = "unknown"
  capacity = 1 << jedec_id[2]
  print("Detected FX flash chip with ID {:02X}{:02X}{:02X} size {}KB".format(jedec_id[0],jedec_id[1],jedec_id[2],capacity // 1024))
  
  oldtime=time.time()
  # when starting partially in a block, preserve the beginning of old block data
  if pagenumber % PAGES_PER_BLOCK:
    blocklen  = pagenumber % PAGES_PER_BLOCK * PAGESIZE
    blockaddr = pagenumber // PAGES_PER_BLOCK * PAGES_PER_BLOCK
    #read partial block data start
    bootloader.write(bytearray([ord("A"), blockaddr >> 8, blockaddr & 0xFF]))
    bootloader.read(1)
    bootloader.write(bytearray([ord("g"), (blocklen >> 8) & 0xFF, blocklen & 0xFF,ord("C")]))
    flashdata = bootloader.read(blocklen) + flashdata
    pagenumber = blockaddr
    
  # when ending partially in a block, preserve the ending of old block data
  if len(flashdata) % BLOCKSIZE:
    blocklen = BLOCKSIZE - len(flashdata) % BLOCKSIZE
    blockaddr = pagenumber + len(flashdata) // PAGESIZE     
    #read partial block data end
    bootloader.write(bytearray([ord("A"), blockaddr >> 8, blockaddr & 0xFF]))
    bootloader.read(1)
    bootloader.write(bytearray([ord("g"), (blocklen >> 8) & 0xFF, blocklen & 0xFF,ord("C")]))
    flashdata += bootloader.read(blocklen)

  ## write to flash cart ##
  blocks = len(flashdata) // BLOCKSIZE
  for block in range (blocks):
    if (block & 1 == 0) or verifyAfterWrite:
      bootloader.write(b"x\xC2") #RGB LED RED, buttons disabled
    else:  
      bootloader.write(b"x\xC0") #RGB LED OFF, buttons disabled
    bootloader.read(1)
    sys.stdout.write("\rWriting block {}/{}  ".format(block + 1,blocks))
    sys.stdout.flush()
    blockaddr = pagenumber + block * BLOCKSIZE // PAGESIZE
    blocklen = BLOCKSIZE
    #write block 
    bootloader.write(bytearray([ord("A"), blockaddr >> 8, blockaddr & 0xFF]))
    bootloader.read(1)
    bootloader.write(bytearray([ord("B"), (blocklen >> 8) & 0xFF, blocklen & 0xFF,ord("C")]))
    bootloader.write(flashdata[block * BLOCKSIZE : block * BLOCKSIZE + blocklen])
    bootloader.read(1)
    if verifyAfterWrite:
      sys.stdout.write("\rVerifying block {}/{}".format(block + 1,blocks))
      sys.stdout.flush()
      bootloader.write(b"x\xC1") #RGB BLUE RED, buttons disabled
      bootloader.read(1)
      bootloader.write(bytearray([ord("A"), blockaddr >> 8, blockaddr & 0xFF]))
      bootloader.read(1)
      bootloader.write(bytearray([ord("g"), (blocklen >> 8) & 0xFF, blocklen & 0xFF,ord("C")]))
      if bootloader.read(blocklen) != flashdata[block * BLOCKSIZE : block * BLOCKSIZE + blocklen]:
        sys.stderr.write(" verify failed!\n\nWrite aborted.")
        bootloader.write(b"x\x40")#RGB LED off, buttons enabled
        bootloader.read(1)
        sys.exit(-1)
  
  #write complete  
  bootloader.write(b"x\x44")#RGB LED GREEN, buttons enabled
  bootloader.read(1)
  time.sleep(0.5)    
  bootloader.write(b"x\x40")#RGB LED off, buttons enabled
  bootloader.read(1)
  bootloader.close()
  print("\rFX Data uploaded successfully")
  
################################################################################

print(title)
  
if (len(sys.argv) != 2) or (os.path.isfile(sys.argv[1]) != True) :
  sys.stderr.write("FX data file not found.\n")

filename = os.path.abspath(sys.argv[1])
  
verifyAfterWrite = True

print('Uploading FX data from file "{}"'.format(filename))
f = open(filename,"rb")
programdata = bytearray(f.read())
f.close()
if len(programdata) % PAGESIZE: 
  programdata += b'\xFF' * (PAGESIZE - (len(programdata) % PAGESIZE))
programpage = MAX_PAGES - (len(programdata) // PAGESIZE)
  
writeFlash(programpage, programdata)
