#!/usr/bin/env python3

import sys
import time
import serial
import binascii


dev = '/dev/ttyAMA0'
baud = 921600
boot_id = 'bbctrl '
verbose = False


def crc16(data):
    crc = 0xffff

    for x in data:
        crc = crc ^ int(x)
        for bit in range(8):
            if crc & 1: crc = (crc >> 1) ^ 0xa001
            else: crc = crc >> 1

    return crc


def avr_crc32(data, length):
    mem = [0xff] * length

    for addr, chunk in data:
        for x in chunk:
            mem[addr] = x
            addr += 1

    return binascii.crc32(bytes(mem))


def read_intel_hex(f):
    base = 0
    pos = 0
    start = 0
    chunk = None

    for line in f:
        line = line.strip()
        if not line or line[0] != ':': continue

        count = int(line[1:3], 16)
        addr = int(line[3:7], 16)
        type = int(line[7:9], 16)
        data = line[9:-2]
        checksum = int(line[-2:], 16)

        if type == 0:
            addr += base
            data = binascii.unhexlify(bytes(data, 'utf8'))

            if chunk is None or pos != addr:
                if chunk is not None: yield (start, chunk)
                start = addr
                chunk = data

            else: chunk += data

            pos = addr + len(data)

        if type == 2: base = int(data, 16) * 16

    if chunk is not None: yield (start, chunk)


def send(data):
    if verbose: print('Sending:', data)
    sp.write(bytes(data, 'utf8'))


def send_int(x, size):
    if verbose: print('Sending: %d', x)
    sp.write((x).to_bytes(size, byteorder = 'big'))


def recv(count):
    data = sp.read(count).decode('utf8')
    if count and verbose: print('Received:', data)
    return data


def recv_int(size):
    x = int.from_bytes(sp.read(size), byteorder = 'big')
    if verbose: print('Received:', x)
    return x


# Read firmware hex file
data = list(read_intel_hex(open(sys.argv[1], 'r')))

# Open serial  port
sp = serial.Serial(dev, baud, timeout = 10)

# Reset AVR
import RPi.GPIO as gpio
gpio.setwarnings(False)
gpio.setmode(gpio.BCM)
gpio.setup(27, gpio.OUT)
gpio.output(27, 0)
gpio.output(27, 1)
gpio.setup(27, gpio.IN, pull_up_down = gpio.PUD_UP)
time.sleep(0.1)

# Sync
for i in range(10): send('\x1b')

# Flush serial
try:
    recv(sp.in_waiting)
except: pass

# Get bootloader ID
send('S')
if boot_id != recv(len(boot_id)):
    raise Exception('Failed to communicate with bootloader')

# Get version
send('V')
major = int(recv(1))
minor = int(recv(1))
print('Bootloader version: %d.%d' % (major, minor))

# If bootloader is new enough compare checksums
if 0 < major or 1 < minor:
    # Get flash length
    send('n')
    flash_len = recv_int(3)

    # Get current flash CRC
    send('X')
    new_crc = avr_crc32(data, flash_len)
    old_crc = recv_int(4)
    if old_crc == new_crc:
        print('Flash already up to date')
        sys.exit(0)

    print('CRC: old=0x%08x new=0x%08x' % (old_crc, new_crc))

    # Erase
    send('e')
    if recv(1) != '\r': raise Exception('Flash erase failed')

# Get page size
send('b')
if recv(1) != 'Y': raise Exception('Cannot get page size')
page_size = recv_int(2)
print('Page size:', page_size)


# Program
print('Programming', end = '')
count = 0
retry = 0
for addr, chunk in data:
    # Set address
    send('H')
    send_int(addr, 3)
    if recv(1) != '\r': raise Exception('Failed to set address')

    while len(chunk):
        sys.stdout.flush()
        page = chunk[0:512]

        # Block command
        send('B')

        # Send block size
        send_int(len(page), 2)

        # Send memory type
        send('F')

        # Send block
        sp.write(page)

        if recv(1) != '\r': raise Exception('Failed to write block')

        # Get and check block CRC
        send('i')
        crc = recv_int(2)
        expect = crc16(page)
        if crc != expect:
            retry += 1
            if retry == 5:
                raise Exception('CRC mismatch %d != %d' % (crc, expect))

            print('x', end = '')
            continue

        count += len(page)
        chunk = chunk[512:]
        retry = 0
        print('.', end = '')


print('done')
print('Wrote %d bytes' % count)

# Exit bootloader
send('E')
