import hid
import tkinter as tk
import tkinter.ttk as ttk
import threading
import time
from enum import IntEnum

BIGSCREEN_VID = 0x35BD
BEYOND_PID = 0x0101
CRASH_PID = 0x1001

class Crash_Type(IntEnum):
    HardFault = 0
    UnusedHandler = 1
    StackOverflow = 2

class HidRunner:
    def __init__(self):
        self.bynd = None
        self.quitting_now = False
        self.connection_change_callback = None
        self.data_callback = None
        self.mthread = threading.Thread(target=self.run)
        self.mthread.start()

    def quit_now(self):
        self.quitting_now = True
        self.mthread.join()

    def set_connection_callback(self, new_callback):
        self.connection_change_callback = new_callback

    def set_data_callback(self, new_callback):
        self.data_callback = new_callback

    def run(self):
        while(not self.quitting_now):
            if(self.bynd is None):
                time.sleep(0.1)
                # Attempt to connect
                try:
                    self.bynd = hid.Device(vid=BIGSCREEN_VID, pid=CRASH_PID)
                    if(self.connection_change_callback is not None):
                        self.connection_change_callback()
                except hid.HIDException:
                    pass
            else:
                # Continuously read
                try:
                    newdata = self.bynd.read(64,timeout=10)
                    if(len(newdata) > 0):
                        if(self.data_callback is not None):
                            self.data_callback(newdata)
                except hid.HIDException:
                    self.bynd = None
                    if(self.connection_change_callback is not None):
                        self.connection_change_callback()

class MainUI:
    def __init__(self):
        self.root = tk.Tk()
        self.root.geometry("300x150")
        self.titlebarvar = tk.StringVar(self.root, "not connected")
        self.crashreasonvar = tk.StringVar(self.root, "-")
        self.irqnumvar = tk.IntVar(self.root, 0)
        self.tasknamevar = tk.StringVar(self.root, "-")
        self.create_ui()
        self.bynd = None
        self.connected = False
        self.busy = False
        self.crash_reason = -1
        self.root.protocol("WM_DELETE_WINDOW", lambda: self.on_close())
        self.hidrunner = HidRunner()
        self.hidrunner.set_connection_callback(self.beyond_connection_change)
        self.hidrunner.set_data_callback(self.new_data_received)
        self.root.mainloop()

    def reset_fields(self):
        self.crashreasonvar.set("-")
        self.irqnumvar.set(0)
        self.tasknamevar.set("-")
        self.crash_reason = -1

    def on_close(self):
        self.hidrunner.quit_now()
        self.root.destroy()

    def send(self, outbytes):
        if(self.hidrunner.bynd is not None):
            self.hidrunner.bynd.send_feature_report(outbytes)
            self.busy = True

    def beyond_connection_change(self):
        if(self.hidrunner.bynd is None):
            self.titlebarvar.set("not connected")
            self.reset_fields()
        else:
            self.titlebarvar.set("connected to Beyond in crash recovery")
            self.send(bytes([0, ord('R')]))

    def new_data_received(self, new_bytes):
        self.busy = False
        if(new_bytes[0] == ord('R')):
            # Crash reason
            self.crash_reason = new_bytes[1]
            if(new_bytes[1] == Crash_Type.HardFault):
                self.crashreasonvar.set("Hard Fault")
                self.irqnumvar.set(new_bytes[2] + 256*new_bytes[3])
            if(new_bytes[1] == Crash_Type.UnusedHandler):
                self.crashreasonvar.set("Unused Interrupt Handler Called")
                self.irqnumvar.set(new_bytes[2] + 256*new_bytes[3])
            if(new_bytes[1] == Crash_Type.StackOverflow):
                self.crashreasonvar.set("FreeRTOS task stack overflow")
                self.irqnumvar.set(0)
                # Also get the task name that crashed
                self.send(bytes([0, ord('T')]))
        if(new_bytes[0] == ord('T')):
            # Crashed task name
            name_length = new_bytes[1]
            self.tasknamevar.set(new_bytes[2:(2+name_length)].decode('ascii'))

    def create_ui(self):
        lbl1 = tk.Label(self.root, textvariable=self.titlebarvar)
        lbl1.grid(row=0, column=0, columnspan = 2, sticky='news')

        lbl2 = tk.Label(self.root, text="Crash Reason:")
        lbl2.grid(row=1, column=0, sticky='news')
        lbl3 = tk.Label(self.root, textvariable=self.crashreasonvar, relief=tk.SUNKEN, borderwidth=2)
        lbl3.grid(row=1, column=1, sticky='news', padx=10)

        lbl4 = tk.Label(self.root, text="IRQ Number:")
        lbl4.grid(row=2, column=0, sticky='news')
        lbl5 = tk.Label(self.root, textvariable=self.irqnumvar, relief=tk.SUNKEN, borderwidth=2)
        lbl5.grid(row=2, column=1, sticky='news', padx=10)

        lbl6 = tk.Label(self.root, text="Crashed Task Name:")
        lbl6.grid(row=3, column=0, sticky='news')
        lbl7 = tk.Label(self.root, textvariable=self.tasknamevar, relief=tk.SUNKEN, borderwidth=2)
        lbl7.grid(row=3, column=1, sticky='news', padx=10)

        self.root.columnconfigure(0,weight=1)
        self.root.columnconfigure(1,weight=1)

if __name__ == '__main__':
    mainui = MainUI()