import sys
import enum
import struct

from PySide6.QtWidgets import QApplication, QMainWindow, QCheckBox, QDialog, QTextEdit, QGridLayout, QColorDialog
from PySide6.QtCore import QFile, QTimer, QThread, Signal, Slot, Qt
from PySide6.QtGui import QColor, QPalette, QFont

import hid
from hid import HIDException

import config_editor_ui

BIGSCREEN_VID = 0x35BD
BEYOND_PID = 0x0101
USER_SIG_LENGTH = 512
CRC_INIT_VAL = 0xFF
CRC_POLY = 0x07

class SigTag(enum.IntEnum):
    Invalid = 0xFF
    Serial = 0x01
    RGB_Color = 0x02
    Fan_Speed = 0x03
    Prox_Disable = 0x04
    Linkbox_v1 = 0x05
    Prox_Cal = 0x06
    FATP_Mode = 0x07
    HMD_Serial = 0x08
    Tracking_Serial = 0x09
    Brightness = 0x0A
    Prox_Thresh = 0x0B
    Prox_Hyst = 0x0C
    EDID_Switch = 0x0D

def crc8(input_data: bytes) -> int:
    initval = CRC_INIT_VAL
    for bb in input_data:
        # for each byte, xor with the current value of CRC
        # then iterate over all 8 bits using the polynomial 
        # to generate crc bits
        initval = initval ^ bb
        for _ in range(8):
            if(initval & 0x80):
                initval = ((initval << 1) & 0xFF) ^ CRC_POLY
            else:
                initval = (initval << 1) & 0xFF
    return initval

class hidreader(QThread):
    HID_INPUT_REPORT_SIZE = 64
    HID_FEATURE_REPORT_SIZE = 64
    HID_TIMEOUT_MS = 10

    data_received = Signal(bytes)
    hid_disconnected = Signal()

    def __init__(self, hid_device: hid.Device):
        self.dev = hid_device
        self.connected = True
        super().__init__()

    def exit_now(self):
        self.time_to_quit = True

    def write(self, wbytes):
        self.time_to_write = True
        self.stuff_to_write = wbytes

    def run(self):
        # periodically grab the input report from the connected HID
        self.time_to_quit = False
        self.time_to_write = False
        self.stuff_to_write = bytes([])
        # infinite loop. read from the device with timeout
        while(not self.time_to_quit):
            try:
                result = self.dev.read(self.HID_INPUT_REPORT_SIZE,
                                       self.HID_TIMEOUT_MS)
            except(HIDException, OSError):
                self.hid_disconnected.emit()
                self.time_to_quit = True
            if(len(result) != 0):
                # We got something, send it to the UI thread
                self.data_received.emit(result)

            if(self.time_to_write):
                # got a push from the UI thread to write something to the HID
                self.time_to_write = False  # stop after one write
                try:
                    self.dev.send_feature_report(self.stuff_to_write)
                except(HIDException, OSError):
                    self.hid_disconnected.emit()
                    self.time_to_quit = True

class RawSigPopUp(QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Raw Signature Data")
        self.editRawSig = QTextEdit(self)
        self.layout = QGridLayout()
        self.layout.addWidget(self.editRawSig, 1, 1, 1, 1)
        self.setLayout(self.layout)
    def setRawText(self, rawsigtext):
        self.editRawSig.setText("<pre>"+rawsigtext+"</pre>")
        #font = QFont()
        #font.setPointSize(8)
        #font.setKerning(True)
        #font.setFixedPitch(True)
        #font.setStyleHint(QFont.TypeWriter)
        #self.editRawSig.setFont(font)
        self.resize(800,600)
        #self.resize()


class mainwin(QMainWindow):
    NUM_ENTRIES = 13

    def __init__(self):
        super(mainwin, self).__init__()
        self.sig_rgb_color = QColor(0,0,0)
        self.load_ui()
        self.function_widgets = [(self.widg.leSerial,),
                                (self.widg.leHMDSerial,),
                                (self.widg.leTrackingSerial,),
                                (self.widg.spinFan,),
                                (self.widg.spinProxCal,),
                                (self.widg.spinRed, self.widg.spinGreen, self.widg.spinBlue, self.widg.btnSelectColor,),
                                (self.widg.chkDisableProx,),
                                (self.widg.chkLinkboxV1,),
                                (self.widg.chkFATP,),
                                (self.widg.slideBright, self.widg.lblBright),
                                (self.widg.spinProxThresh,),
                                (self.widg.spinProxHyst,),
                                (self.widg.cbEdid,),]
        self.function_indexes = {SigTag.Serial:0,
                                SigTag.HMD_Serial:1,
                                SigTag.Tracking_Serial:2,
                                SigTag.Fan_Speed:3,
                                SigTag.Prox_Cal:4,
                                SigTag.RGB_Color:5,
                                SigTag.Prox_Disable:6,
                                SigTag.Linkbox_v1:7,
                                SigTag.FATP_Mode:8,
                                SigTag.Brightness:9,
                                SigTag.Prox_Thresh:10,
                                SigTag.Prox_Hyst:11,
                                SigTag.EDID_Switch:12,}

        self.create_enable_buttons()
        self.widg.btnLoad.clicked.connect(self.load_from_hmd)
        self.widg.btnSave.clicked.connect(self.save_to_hmd)
        self.widg.btnRawData.clicked.connect(self.showUserSignaturePopup)
        self.widg.btnSelectColor.clicked.connect(self.color_picker)
        self.widg.spinRed.valueChanged.connect(self.spin_color_changed)
        self.widg.spinGreen.valueChanged.connect(self.spin_color_changed)
        self.widg.spinBlue.valueChanged.connect(self.spin_color_changed)
        self.widg.slideBright.valueChanged.connect(self.bright_slider_changed)
        self.widg.cbEdid.addItems(['Both 75Hz and 90Hz', '90Hz only', '75Hz only'])
        

    # Open the UI file and create the main window
    def load_ui(self):
        self.widg = config_editor_ui.Ui_MainWindow()
        self.widg.setupUi(self)
        self.setWindowTitle('Bigscreen Config Utility')
        self.update_label_color(self.sig_rgb_color)
    
    def create_enable_buttons(self):
        #self.widg.gridLayout.removeWidget(self.widg.lblDeleteMe)
        self.widg.lblDeleteMe.setVisible(False)
        self.enable_buttons = [QCheckBox(self.widg.centralwidget) for _ in range(self.NUM_ENTRIES)]
        for btn, i in zip(self.enable_buttons, range(self.NUM_ENTRIES)):
            btn.setText('Enabled')
            btn.setChecked(True)
            self.widg.gridLayout.addWidget(btn, 2*i, 0, 1, 1)
            btn.clicked.connect(self.en_functions)

        self.functions_enabled = [True]*self.NUM_ENTRIES

    def bright_slider_changed(self):
        new_bright = self.widg.slideBright.value()
        self.widg.lblBright.setText(str(new_bright))

    def update_label_color(self, newcolor: QColor):
        self.widg.lblColorSample.setStyleSheet("QLabel { background-color: rgb("
            +"{},{},{}".format(newcolor.red(), newcolor.green(),newcolor.blue())
            +")}")

    def color_picker(self):
        self.sigColorDialog = QColorDialog()
        self.sigColorDialog.currentColorChanged.connect(self.new_sig_color_callback)
        self.sigColorDialog.open()

    def new_sig_color_callback(self):
        newcolor = self.sigColorDialog.currentColor()
        self.new_sig_color(newcolor)

    def new_sig_color(self, newcolor: QColor):
        self.widg.spinRed.setValue(newcolor.red())
        self.widg.spinGreen.setValue(newcolor.green())
        self.widg.spinBlue.setValue(newcolor.blue())
        self.sig_rgb_color = newcolor
        self.update_label_color(self.sig_rgb_color)

    def spin_color_changed(self, newval):
        #print(newval)
        self.sig_rgb_color = QColor(self.widg.spinRed.value(), self.widg.spinGreen.value(), self.widg.spinBlue.value())
        self.update_label_color(self.sig_rgb_color)

    def load_from_hmd(self):
        try:
            self.hid_device = hid.Device(vid = BIGSCREEN_VID, pid = BEYOND_PID)
        except:
            self.statusBar().showMessage("Could not connect to HMD.")
            return
        self.user_sig_data = bytearray([0]*USER_SIG_LENGTH)
        self.current_sig_page = 0
        self.statusBar().showMessage("Now loading configuration from HMD ({}/16)...".format(self.current_sig_page + 1))
        # create a HID thread and send the read config message
        self.hidthread = hidreader(self.hid_device)
        self.hidthread.data_received.connect(self.hid_data_received_load)
        self.hidthread.start()
        self.hmdreadstarttimer = QTimer()
        self.hmdreadstarttimer.setSingleShot(True)
        self.hmdreadstarttimer.timeout.connect(self.begin_hmd_read)
        self.hmdreadstarttimer.start(100)

    def begin_hmd_read(self):
        del self.hmdreadstarttimer
        self.hidthread.write(bytes([0, ord('U'), self.current_sig_page]))
        # Start a timer to have a timeout failure
        self.hmdreadtimer = QTimer()
        self.hmdreadtimer.timeout.connect(self.read_timed_out)
        self.hmdreadtimer.setSingleShot(True)
        self.hmdreadtimer.start(1000)
    
    def read_timed_out(self):
        self.statusBar().showMessage("HMD response timed out")
        self.hidthread.exit_now()
        while(self.hidthread.isRunning()):
            pass
        self.hid_device.close() # and close the connection

    def hid_data_received_load(self, hid_data: bytes):
        if(hid_data[0] == ord('U')):
            self.hmdreadtimer.stop()
            # User signature reply packet
            user_sig_reply_len = hid_data[1]
            user_sig_reply = hid_data[2:(2+user_sig_reply_len)]
            self.user_sig_data[32*self.current_sig_page: (32*(self.current_sig_page+1))] = user_sig_reply
            if(self.current_sig_page < 15):
                # get the next page
                self.current_sig_page = self.current_sig_page + 1
                self.statusBar().showMessage("Now loading configuration from HMD ({}/16)...".format(self.current_sig_page + 1))
                self.hidthread.write(bytes([0, ord('U'), self.current_sig_page]))
                self.hmdreadtimer.start(1000)
            else:
                # Done with hid thread, can stop it
                self.hidthread.exit_now()
                while(self.hidthread.isRunning()):
                    pass
                self.hid_device.close() # and close the connection
                # Finished with retrieving the config section, time to parse it
                if(self.parse_sig()):
                    self.statusBar().showMessage("Loaded config from HMD")
                    for i in range(self.NUM_ENTRIES):
                        self.enable_buttons[i].setChecked(self.functions_enabled[i])
                    self.en_functions()
                    # self.showUserSignaturePopup()
                    # self.printRawUserSignature()
                else:
                    self.statusBar().showMessage("Invalid signature block")

    def printRawUserSignature(self):
        for i in range(int(USER_SIG_LENGTH/16)):
            print('{:04X}: '.format(i*16), end='')
            print(''.join(['{:02X} '.format(bb) for bb in self.user_sig_data[16*i:(16*(i+1))]]))

    def showUserSignaturePopup(self):
        if(hasattr(self, 'user_sig_data')):
            txtt = ''
            for i in range(int(USER_SIG_LENGTH/16)):
                txtt = txtt + '{:04X}: '.format(i*16)
                txtt = txtt + ''.join(['{:02X} '.format(bb) for bb in self.user_sig_data[16*i:(16*(i+1))]])
                for bb in self.user_sig_data[16*i:(16*(i+1))]:
                    thisbyte = bytes([bb]).decode('utf-8', errors='replace')
                    if(thisbyte.isprintable()):
                        txtt = txtt + thisbyte
                    else:
                        txtt = txtt + '.'
                txtt = txtt + '\n'
        else:
            txtt = 'No data received'
        #print(txtt,end='')
        siggypoppy = RawSigPopUp()
        siggypoppy.setRawText(txtt)
        siggypoppy.exec()

    def parse_sig(self):
        # reads the signature block and extracts the config data
        # data is in TLVC (Tag, Length, Value, CRC) format
        # Byte0: Tag, tells you what data field is saved
        # Byte1: Length, number of bytes in the Value
        # Byte2 to Byte 2+Length-1: Value, arbitrary data saved for this field
        # Byte 2+Length: CRC, 8-bit CRC code generated with polynomial 0x07 over all bytes (including tag)
        #               with an initial value of 0xFF

        # set all of the fields to false until we actually decode that field
        self.functions_enabled = [False]*self.NUM_ENTRIES
        sig_ptr = 0
        while(sig_ptr < USER_SIG_LENGTH):
            if(self.user_sig_data[sig_ptr] == SigTag.Invalid):
                # we reached the end of saved data, so this was valid
                # even if there's no data saved this can be a valid signature (all 0xFF)
                return True
            # Ensure there's enough room for a 1-byte field
            if(sig_ptr > (USER_SIG_LENGTH - 4)):
                # Tag will overrun end of signature region
                return False
            tag = self.user_sig_data[sig_ptr]
            taglen = self.user_sig_data[sig_ptr+1]
            # now ensure there's enough room for THIS field. Use the length data.
            if(sig_ptr > (USER_SIG_LENGTH - (3 + taglen))):
                return False
            tagval = self.user_sig_data[sig_ptr+2:(sig_ptr+2+taglen)]
            tagcrc = self.user_sig_data[sig_ptr+2+taglen]
            # check the CRC
            computecrc = crc8(bytes([tag, taglen])+bytes(tagval))
            if(tagcrc != computecrc):
                return False
            # Check if the tag is in our database
            if(tag == SigTag.Serial):
                self.sig_serial = tagval.decode('ascii')
                self.widg.leSerial.setText(self.sig_serial)
                self.functions_enabled[self.function_indexes[SigTag.Serial]] = True
            if(tag == SigTag.HMD_Serial):
                self.sig_hmd_serial = tagval.decode('ascii')
                self.widg.leHMDSerial.setText(self.sig_hmd_serial)
                self.functions_enabled[self.function_indexes[SigTag.HMD_Serial]] = True
            if(tag == SigTag.Tracking_Serial):
                self.sig_tracking_serial = tagval.decode('ascii')
                self.widg.leTrackingSerial.setText(self.sig_tracking_serial)
                self.functions_enabled[self.function_indexes[SigTag.Tracking_Serial]] = True                
            if(tag == SigTag.Fan_Speed):
                if(taglen != 1):
                    return False
                self.sig_fan_speed = struct.unpack("<B",tagval)[0]
                self.functions_enabled[self.function_indexes[SigTag.Fan_Speed]] = True
                self.widg.spinFan.setValue(self.sig_fan_speed)
            if(tag == SigTag.Prox_Cal):
                if(taglen != 2):
                    return False
                self.sig_prox_cal = struct.unpack("<H", tagval)[0]
                self.functions_enabled[self.function_indexes[SigTag.Prox_Cal]] = True
                self.widg.spinProxCal.setValue(self.sig_prox_cal)
            if(tag == SigTag.Prox_Thresh):
                if(taglen != 2):
                    return False
                self.sig_prox_thresh = struct.unpack("<H", tagval)[0]
                self.functions_enabled[self.function_indexes[SigTag.Prox_Thresh]] = True
                self.widg.spinProxThresh.setValue(self.sig_prox_thresh)
            if(tag == SigTag.Prox_Hyst):
                if(taglen != 2):
                    return False
                self.sig_prox_hyst = struct.unpack("<H", tagval)[0]
                self.functions_enabled[self.function_indexes[SigTag.Prox_Hyst]] = True
                self.widg.spinProxHyst.setValue(self.sig_prox_hyst)
            if(tag == SigTag.RGB_Color):
                if(taglen != 3):
                    return False
                self.sig_rgb_color = QColor(tagval[0], tagval[1], tagval[2])
                self.new_sig_color(self.sig_rgb_color)
                self.functions_enabled[self.function_indexes[SigTag.RGB_Color]] = True
            if(tag == SigTag.Prox_Disable):
                if(taglen != 1):
                    return False
                if(type(tagval) == bytearray or type(tagval) == bytes):
                    self.sig_prox_disable = int(tagval[0])
                else:
                    self.sig_prox_disable = tagval
                if(self.sig_prox_disable == 0x01):
                    self.widg.chkDisableProx.setChecked(True)
                else:
                    self.widg.chkDisableProx.setChecked(False)
                self.functions_enabled[self.function_indexes[SigTag.Prox_Disable]] = True
            if(tag == SigTag.Linkbox_v1):
                if(taglen != 1):
                    return False
                if(type(tagval) == bytearray or type(tagval) == bytes):
                    self.sig_linkbox_v1 = int(tagval[0])
                else:
                    self.sig_linkbox_v1 = tagval
                if(self.sig_linkbox_v1 == 0x01):
                    self.widg.chkLinkboxV1.setChecked(True)
                else:
                    self.widg.chkLinkboxV1.setChecked(False)
                self.functions_enabled[self.function_indexes[SigTag.Linkbox_v1]] = True
            if(tag == SigTag.FATP_Mode):
                if(taglen != 1):
                    return False
                if(type(tagval) == bytearray or type(tagval) == bytes):
                    self.sig_fatp_mode = int(tagval[0])
                else:
                    self.sig_fatp_mode = tagval
                if(self.sig_fatp_mode == 0x01):
                    self.widg.chkFATP.setChecked(True)
                else:
                    self.widg.chkFATP.setChecked(False)
                self.functions_enabled[self.function_indexes[SigTag.FATP_Mode]] = True
            if(tag == SigTag.Brightness):
                if(taglen != 2):
                    return False
                self.sig_brightness = struct.unpack("<H", tagval)[0]
                self.functions_enabled[self.function_indexes[SigTag.Brightness]] = True
                self.sig_brightness = int(float(self.sig_brightness) * 100.0 / 1023.0)
                if(self.sig_brightness > 100):
                    self.sig_brightness = 100
                if(self.sig_brightness < 0):
                    self.sig_brightness = 0
                self.widg.slideBright.setValue(self.sig_brightness)
            if(tag == SigTag.EDID_Switch):
                if(taglen != 1):
                    return False
                if(type(tagval) == bytearray or type(tagval) == bytes):
                    self.sig_edid = int(tagval[0])
                else:
                    self.sig_edid = tagval
                self.functions_enabled[self.function_indexes[SigTag.EDID_Switch]] = True
                self.widg.cbEdid.setCurrentIndex(self.sig_edid)
            # any other tag should be ignored. can still be a valid config region, just means
            # there's a new field type that we don't know about yet
            sig_ptr = sig_ptr + 3 + taglen # length of data, plus the 3 fixed bytes (tag, length, crc)

    def save_to_hmd(self):
        self.create_signature()

        self.statusBar().showMessage("Saved to HMD")

    def create_signature(self):
        # check what's been enabled, add it to the big buffer
        sig_ptr = 0
        self.user_sig_data = bytearray([0xFF]*USER_SIG_LENGTH)
        if(self.enable_buttons[self.function_indexes[SigTag.Serial]].isChecked()):
            # serial number
            newsig = self.widg.leSerial.text().strip().encode()
            if(len(newsig) > 0):
                newblock = self.create_field(SigTag.Serial, newsig)
                self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
                sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.HMD_Serial]].isChecked()):
            newsig = self.widg.leHMDSerial.text().strip().encode()
            if(len(newsig) > 0):
                newblock = self.create_field(SigTag.HMD_Serial, newsig)
                self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
                sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.Tracking_Serial]].isChecked()):
            newsig = self.widg.leTrackingSerial.text().strip().encode()
            if(len(newsig) > 0):
                newblock = self.create_field(SigTag.Tracking_Serial, newsig)
                self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
                sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.Fan_Speed]].isChecked()):
            # fan speed
            newblock = self.create_field(SigTag.Fan_Speed, bytes([self.widg.spinFan.value()]))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.Prox_Cal]].isChecked()):
            # proximity cal
            newblock = self.create_field(SigTag.Prox_Cal, struct.pack("<H", self.widg.spinProxCal.value()))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.RGB_Color]].isChecked()):
            # rgb led
            newblock = self.create_field(SigTag.RGB_Color, bytes([self.widg.spinRed.value(), self.widg.spinGreen.value(), self.widg.spinBlue.value()]))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.Prox_Disable]].isChecked()):
            # disable prox
            val = bytes([0x01]) if self.widg.chkDisableProx.isChecked() else bytes([0x00])
            newblock = self.create_field(SigTag.Prox_Disable, val)
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)
        if(self.enable_buttons[self.function_indexes[SigTag.Linkbox_v1]].isChecked()):
            # linkbox v1
            val = bytes([0x01]) if self.widg.chkLinkboxV1.isChecked() else bytes([0x00])
            newblock = self.create_field(SigTag.Linkbox_v1, val)
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)    
        if(self.enable_buttons[self.function_indexes[SigTag.FATP_Mode]].isChecked()):
            # FATP mode
            val = bytes([0x01]) if self.widg.chkFATP.isChecked() else bytes([0x00])
            newblock = self.create_field(SigTag.FATP_Mode, val)
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)

        if(self.enable_buttons[self.function_indexes[SigTag.Brightness]].isChecked()):
            # Brightness of displays
            newbright = float(self.widg.slideBright.value()) / 100.0
            newbright = newbright * 1023.0
            newbright = int(newbright)
            if(newbright > 1023):
                newbright = 1023
            if(newbright < 1):
                newbright = 1
            newblock = self.create_field(SigTag.Brightness, struct.pack("<H", newbright))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)

        if(self.enable_buttons[self.function_indexes[SigTag.Prox_Thresh]].isChecked()):
            # Proximity sensor threshold
            newblock = self.create_field(SigTag.Prox_Thresh, struct.pack("<H", self.widg.spinProxThresh.value()))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)

        if(self.enable_buttons[self.function_indexes[SigTag.Prox_Hyst]].isChecked()):
            # Proximity sensor hysteresis
            newblock = self.create_field(SigTag.Prox_Hyst, struct.pack("<H", self.widg.spinProxHyst.value()))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)   

        if(self.enable_buttons[self.function_indexes[SigTag.EDID_Switch]].isChecked()):
            newblock = self.create_field(SigTag.EDID_Switch, bytes([self.widg.cbEdid.currentIndex()]))
            self.user_sig_data[sig_ptr:sig_ptr + len(newblock)] = newblock
            sig_ptr = sig_ptr + len(newblock)
        #self.showUserSignaturePopup()

        # begin sending the new config to the HMD
        try:
            self.hid_device = hid.Device(vid = BIGSCREEN_VID, pid = BEYOND_PID)
        except:
            self.statusBar().showMessage("Could not connect to HMD.")
            return
        self.hidthread = hidreader(self.hid_device)
        self.hidthread.data_received.connect(self.hid_data_received_save)
        self.hidthread.start()
        self.hmdsavestarttimer = QTimer()
        self.hmdsavestarttimer.setSingleShot(True)
        self.hmdsavestarttimer.timeout.connect(self.begin_hmd_save)
        self.hmdsavestarttimer.start(100)

    def begin_hmd_save(self):
        del self.hmdsavestarttimer
        self.current_sig_page = 0
        self.hidthread.write(bytes([0, ord('W'), self.current_sig_page]) + self.user_sig_data[32*self.current_sig_page:32*(self.current_sig_page+1)])
        self.statusBar().showMessage("Now saving configuration to HMD ({}/16)...".format(self.current_sig_page + 1))
        # Start a timer to have a timeout failure
        self.hmdsavetimer = QTimer()
        self.hmdsavetimer.timeout.connect(self.save_timed_out)
        self.hmdsavetimer.setSingleShot(True)
        self.hmdsavetimer.start(1000)

    def save_timed_out(self):
        self.statusBar().showMessage("HMD response timed out")
        self.hidthread.exit_now()
        while(self.hidthread.isRunning()):
            pass
        self.hid_device.close() # and close the connection

    def hid_data_received_save(self, hid_data: bytes):
        if(hid_data[0] == ord('$')):
            # got a success response, go on to the next
            self.hmdsavetimer.stop()
            if(self.current_sig_page < 15):
                self.current_sig_page = self.current_sig_page + 1
                self.hidthread.write(bytes([0, ord('W'), self.current_sig_page]) + self.user_sig_data[32*self.current_sig_page:32*(self.current_sig_page+1)])
                self.statusBar().showMessage("Now saving configuration to HMD ({}/16)...".format(self.current_sig_page + 1))
                self.hmdsavetimer.start(1000)
            if(self.current_sig_page == 15): 
                # we now need to commit this buffer to memory
                self.current_sig_page = self.current_sig_page + 1
                self.hidthread.write(bytes([0, ord('V')]))
                self.hmdsavetimer.start(1000)
            if(self.current_sig_page == 16):
                self.statusBar().showMessage("Save complete!")
                self.hidthread.exit_now()
                while(self.hidthread.isRunning()):
                    pass
                self.hid_device.close() # and close the connection

    def create_field(self, tag: SigTag, data: bytes):
        retbytes = bytes([tag, len(data)]) + data
        crc = crc8(retbytes)
        return retbytes + bytes([crc])


    def en_functions(self):
        # scan the enable buttons and enable/disable the widgets associated with each function
        for i in range(self.NUM_ENTRIES):
            this_enabled = self.enable_buttons[i].isChecked()
            for widg in self.function_widgets[i]:
                widg.setEnabled(this_enabled)
            self.functions_enabled[i] = this_enabled
            if(this_enabled):
                self.enable_buttons[i].setText('Enabled')
            else:
                self.enable_buttons[i].setText('Disabled')

if __name__ == '__main__':
    app = QApplication([])
    widget = mainwin()
    widget.show()
    sys.exit(app.exec())


