Uploads firmware to an `AlarmDecoder`_ device.

        :param file_path: firmware file path
        :type file_path: string
        :param progress_callback: callback function used to report progress
        :type progress_callback: function

        :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError`

def upload(device, file_path, progress_callback=None, debug=False):
        """
        Uploads firmware to an `AlarmDecoder`_ device.

        :param file_path: firmware file path
        :type file_path: string
        :param progress_callback: callback function used to report progress
        :type progress_callback: function

        :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError`
        """

        def progress_stage(stage, **kwargs):
            """Callback to update progress for the specified stage."""
            if progress_callback is not None:
                progress_callback(stage, **kwargs)

            return stage

        if device is None:
            raise NoDeviceError('No device specified for firmware upload.')

        fds = [device._device.fileno()]

        # Read firmware file into memory
        try:
            write_queue = read_firmware_file(file_path)
        except IOError as err:
            stage = progress_stage(Firmware.STAGE_ERROR, error=str(err))
            return

        data_read = ''
        got_response = False
        running = True
        stage = progress_stage(Firmware.STAGE_START)

        if device.is_reader_alive():
            # Close the reader thread and wait for it to die, otherwise
            # it interferes with our reading.
            device.stop_reader()
            while device._read_thread.is_alive():
                stage = progress_stage(Firmware.STAGE_WAITING)
                time.sleep(0.5)

            time.sleep(3)

        try:
            while running:
                rr, wr, _ = select.select(fds, fds, [], 0.5)

                if len(rr) != 0:
                    response = Firmware.read(device)

                    for c in response:
                        # HACK: Python 3 / PySerial hack.
                        if isinstance(c, int):
                            c = chr(c)

                        if c == '\xff' or c == '\r':    # HACK: odd case for our mystery \xff byte.
                            # Boot started, start looking for the !boot message
                            if data_read.startswith("!sn"):
                                stage = progress_stage(Firmware.STAGE_BOOT)
                            # Entered bootloader upload mode, start uploading
                            elif data_read.startswith("!load"):
                                got_response = True
                                stage = progress_stage(Firmware.STAGE_UPLOADING)
                            # Checksum error
                            elif data_read == '!ce':
                                running = False
                                raise UploadChecksumError("Checksum error in {0}".format(file_path))
                            # Bad data
                            elif data_read == '!no':
                                running = False
                                raise UploadError("Incorrect data sent to bootloader.")
                            # Firmware upload complete
                            elif data_read == '!ok':
                                running = False
                                stage = progress_stage(Firmware.STAGE_DONE)
                            # All other responses are valid during upload.
                            else:
                                got_response = True
                                if stage == Firmware.STAGE_UPLOADING:
                                    progress_stage(stage)

                            data_read = ''
                        elif c == '\n':
                            pass
                        else:
                            data_read += c

                if len(wr) != 0:
                    # Reboot device
                    if stage in [Firmware.STAGE_START, Firmware.STAGE_WAITING]:
                        device.write('=')
                        stage = progress_stage(Firmware.STAGE_WAITING_ON_LOADER)

                    # Enter bootloader
                    elif stage == Firmware.STAGE_BOOT:
                        device.write('=')
                        stage = progress_stage(Firmware.STAGE_LOAD)

                    # Upload firmware
                    elif stage == Firmware.STAGE_UPLOADING:
                        if len(write_queue) > 0 and got_response == True:
                            got_response = False
                            device.write(write_queue.popleft())

        except UploadError as err:
            stage = progress_stage(Firmware.STAGE_ERROR, error=str(err))
        else:
            stage = progress_stage(Firmware.STAGE_DONE)