#!/usr/bin/env python

#
# MidNite Firmware Uploader for Linux
# Version 1.0 8/15/2019
# Email: earl@microcontrollerelectronics.com
#

'''

The Classic connects to a PC (via a USB cable) which Linux sees as a serial port (usually /dev/ttyACM0). This python script takes a firmware file (.ctl file for the Classic and/or the .rem file for the remote MNGP) and loads it via that serial interface. The latest firmware can be found here: http://www.midnitesolar.com/firmwareIndex.php)
Download the windows version and extract it, only needed are the .ctl and .rem files.

When the Classic powers up, its USB port comes alive and the host PC can quickly make a connection.  It waits a few seconds for a command, if there, to update the Classic or MNGP remote firwmare, if it does not get the command it will continue its boot process. 

To send a .ctl file for the Classic, it only needs a special 12 ASCII digit command sent right after bootup to tell the Classic it has new firmware to load. If it gets that command, it will wait for the .ctl file to be sent. If it does not get that command it will continue with the boot (start up) process.

To send a .rem (MNGP firmware) file,  a special 12 ASCII digit command is sent right after bootup to tell the Classic is has new firmware to load to the MNGP. This command is different that the command for the .ctl file and it must be sent two times. The first time it is sent, it signals the Classic to create a bridge through the Classic's USB to the top serial port plug on the Classic control board to the MNGP. When the command is sent the second time, the MNGP can receive it through that bridge just made.

This all happens within the first 2.5 or 3 seconds after the Classic powers up.  If the Classic and MNGP do not hear the special 12 ASCII character withing the first 3 seconds or so, they continue booting normally.

Actually, not only does the Classic's USB jack listen for these commands and code update, but so does the bottom of the three jacks but it takes RS-232 instead which would need an adapter of some sort and you have to kind of time things with the Classic power and hitting the buttons on the host PC, typically.

If the Classic successfully reads the command to update, the Classic's flash will be erased and won't work until it is successfully updated.  The bootloader in the Classic or MNGP will not be erased though so you should not be able to brick it.


To run this script:

./mfu.py Classic_Control_2193_150.ctl 

or

./mfu.py MNGP-2186.rem
 

The script will automatically wait for and detect the serial port. Also based on the file name it will detect what code to send to load the firmware.

Make sure the cable to the Classic is plugged in, then power on the classic.

'''

import os, sys, time, glob

#
# Variables which can be adjusted
#
#
FLASH_ERASE_WAIT_TIME = 2.0            # Wait time after sending firmware update code for flash to erase
MAX_FAIL_COUNT        = 5              # Maximum number of failures allowed for lost packets before ending
MAX_SER_WRITE_FAIL    = 5              # Maximum number of Serial Write failures allowed before ending
BAUD_RATE             = "57600"        # Serial Baud Rate 
CLASSIC_SECRET        = "uPlDcLSaLLfl" # Classic     update, everything but the bootloader (flash)
MNGP_SECRET           = "uPlDrEMaLLfl" # MNGP Remote update, everything but the bootloader (flash)
MNGP_WAIT_TIME        = 1.0            # MNGP Remote update needs to send secret again after this wait time
SERIAL_DEVICE         = ""             # Specify or leave blank to scan for it
SERIAL_SCAN_TIME      = .5             # Time to wait between scans for serial device
READ_TIMEOUT          = 2              # Serial Port Read Timeout value in seconds or None or 0 for non-blocking
WRITE_TIMEOUT         = 2              # Serial Port Write Timeout (same values as Read Timeout)
SERIAL_BUFFER_WAIT    = 1              # Time to wait if Serial Buffers not empty

print("MidNite Firmware Updater")

try:
  import serial
except Exception:
  print("Error: Unable to import python serial module (pyserial).")
  print("To install it: pip install pyserial")
  sys.exit(1)

def ByteToHex( byteStr ):
  return ''.join( [ "%02X " % ord( x ) for x in byteStr ] ).strip()

def HexToByte( hexStr ):
  bytes  = []
  hexStr = ''.join( hexStr.split(" ") )
  for i in range(0, len(hexStr), 2):
    bytes.append( chr( int (hexStr[i:i+2], 16 ) ) )
  return ''.join( bytes )

if (len(sys.argv) > 1):
  filename = sys.argv[1]
  if (filename.find("MNGP") != -1):
    secret = MNGP_SECRET
  else:
    secret = CLASSIC_SECRET
else:
  print("Syntax: %s firmware_file [CLASSIC | MNGP]") % sys.argv[0]
  sys.exit()

if (len(sys.argv) > 2):
  if (sys.argv[2] == "MNGP"):
    secret = MNGP_SECRET
    print("Updating MNGP Firmware!")
  else:
    secret = CLASSIC_SECRET
    print("Updating Classic Firmware!")

try:
  fh = open(filename, 'r')
except IOError, e:
  print("Error opening: %s") % (filename)
  print e
  sys.exit(1)
try:
 raw = HexToByte(fh.read())
except:
  print("Error: %s") % sys.exc_info()[0]
  sys.exit(1)

fh.close()

# Save the raw .bin file from the .ctl file
#dst     = filename + ".bin"
#outfile = open(dst,'w')
#outfile.write(raw)
#outfile.close()

print("Firmware file: %s")       % (filename)
print("Firmware size: %d bytes") % (len(raw))

def serial_scan():
  if (SERIAL_DEVICE != ""):
    scan = glob.glob(SERIAL_DEVICE)
    if (len(scan) == 0): return ""
    else: return SERIAL_DEVICE
  dev  = "/dev/ttyACM*"
  scan = glob.glob(dev)
  if (len(scan) == 0):
    dev  = '/dev/ttyUSB*'
    scan = glob.glob(dev)
    if (len(scan) == 0): return ""
  return scan[0]
      
print("Scanning/Waiting for the Serial Device...")

dev = serial_scan()
while(dev == ""):
# print("Unable to find any Serial ports while scanning for /dev/[ttyACM*|ttyUSB*]")
# print("Sleeping...")
  time.sleep(SERIAL_SCAN_TIME)
  dev = serial_scan()

print("Using Serial Device: %s") % (dev)

try:
  ser = serial.Serial(port=dev,baudrate=BAUD_RATE,parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,bytesize=serial.EIGHTBITS,write_timeout=WRITE_TIMEOUT,timeout=READ_TIMEOUT)
except serial.SerialException, e:
  print("\nSerial Open Failed!")
  print e
  sys.exit(1)	

print("Connected to: " + ser.portstr + " at " + BAUD_RATE + " BAUD")

def sendser(msg):
  global ser
  try:
    ser.write(msg)
  except serial.SerialTimeoutException, e:
    print("\nWrite Failed: SerialTimeoutException!")
    print e
    print("\nEnded due to Error.")
    sys.exit(1)
  except serial.SerialException, e:
    print("\nWrite Failed: SerialException!")
    print e
    print("\nEnded due to Error.")
    sys.exit(1)
  except ValueError, e:
    print("\nWrite Failed: ValueError!")
    print e
    print("\nEnded due to Error.")
    sys.exit(1)

print("Sending upload command: %s") % (secret)
sendser(secret)

if (secret == MNGP_SECRET):
  print("Sleeping for Classic to MNGP bridge...")
  time.sleep(MNGP_WAIT_TIME)
  print("Sending repeat of upload command: %s") % (secret)
  sendser(secret) 

#while(ser.out_waiting > 0):
#  print("Waiting for Serial output buffer to flush..")
#  time.sleep(SERIAL_BUFFER_WAIT)
#  ser.flush()

#while(ser.in_waiting > 0):
#  print("Waiting for Serial input buffer to flush..")
#  time.sleep(SERIAL_BUFFER_WAIT)
#  ser.read()

print("Sleeping for flash erase...")
time.sleep(FLASH_ERASE_WAIT_TIME)

print("Uploading Firmware...")
print("Frame Sent Legend: ?=None .=Ack x=Nak")
		
idx       = 0
framesize = 0
r         = ""
failed    = 0
fl        = len(raw) 
tb        = 0
wf        = 0

while (idx < fl):
  framesize = ord(raw[idx])*256+ord(raw[idx+1]) + 2
# print("Sending %-02d bytes  Left: %-05d  Sent: %s") % (framesize,(fl - idx),tb)
  sendser(raw[idx:(idx+framesize)])
  while(ser.out_waiting > 0):
    print("\nWaiting for Serial output buffer to flush..\n")
    time.sleep(SERIAL_BUFFER_WAIT)
    ser.flush()
  try:
    r = ser.read()
    if (len(r) == 0):
      print("?"),
      sys.stdout.flush()
      failed = failed + 1
      if (failed >= MAX_FAIL_COUNT):
        print("\nUpdate Failed: No Response!")
        sys.exit(1)
      continue
    if (ord(r) == 0x11):
      idx = idx + framesize
      tb  = tb + framesize
      print("."),
      sys.stdout.flush()
    elif (ord(r) == 0x22):
      print("x"),
      sys.stdout.flush()
      failed = failed + 1
      if (failed >= MAX_FAIL_COUNT):
        print("\nUpdate Failed: Lost packets!")
        sys.exit(1)
    else:
      for x in r: print ("%s") % (x.encode('hex')),
      print("\nUpdate Failed: No ACK byte received!")
      sys.exit(1)
  except serial.SerialException, e:
    print("\nSerial Write Exception..")
    print e
    sys.exit(1)
  except TypeError, e:
    print("\nSerial Write TypeError..")
    print e
    sys.exit(1)
  except ValueError, e:
    print("\nSerial Write ValueError..")
    print e
    sys.exit(1)

print("\nFinished. Success!")
ser.close()
sys.exit(0)
