docs: private void sendFirmware(final BluetoothGatt gatt) throws RemoteDfuException,
              DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
  		// Send the number of packets of firmware before receiving a receipt notification
  		int numberOfPacketsBeforeNotification = mPacketsBeforeNotification;
  		if (numberOfPacketsBeforeNotification > 0) {
  			setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
  			mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
  					"Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
  		}
  
  		// We are ready to start sending the new firmware.
  
  		logi("Setting object to Data (Op Code = 6, Type = 2)");
  		final ObjectInfo info = selectObject(OBJECT_DATA);
  		logi(String.format(Locale.US,
  				"Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
  		mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US,
  				"Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
  		mProgressInfo.setMaxObjectSizeInBytes(info.maxSize);
  
  		// Number of chunks in which the data will be sent
  		final int chunkCount = (mImageSizeInBytes + info.maxSize - 1) / info.maxSize;
  		int currentChunk = 0;
  		boolean resumeSendingData = false;
  
  		// Can we resume? If the offset obtained from the device is greater then zero we can compare it with the local CRC
  		// and resume sending the data.
  		if (info.offset > 0) {
  			try {
  				currentChunk = info.offset / info.maxSize;
  				int bytesSentAndExecuted = info.maxSize * currentChunk;
  				int bytesSentNotExecuted = info.offset - bytesSentAndExecuted;
  
  				// If the offset is dividable by maxSize, assume that the last page was not executed
  				if (bytesSentNotExecuted == 0) {
  					bytesSentAndExecuted -= info.maxSize;
  					bytesSentNotExecuted = info.maxSize;
  				}
  
  				// Read the same number of bytes from the current init packet to calculate local CRC32
  				if (bytesSentAndExecuted > 0) {
  					mFirmwareStream.read(new byte[bytesSentAndExecuted]); // Read executed bytes
  					mFirmwareStream.mark(info.maxSize); // Mark here
  				}
  				// Here the bytesSentNotExecuted is for sure greater then 0
  				mFirmwareStream.read(new byte[bytesSentNotExecuted]); // Read the rest
  
  				// Calculate the CRC32
  				final int crc = (int) (((ArchiveInputStream) mFirmwareStream).getCrc32() & 0xFFFFFFFFL);
  
  				if (crc == info.CRC32) {
  					logi(info.offset + " bytes of data sent before, CRC match");
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
  							info.offset + " bytes of data sent before, CRC match");
  					mProgressInfo.setBytesSent(info.offset);
  					mProgressInfo.setBytesReceived(info.offset);
  
  					// If the whole page was sent and CRC match, we have to make sure it was executed
  					if (bytesSentNotExecuted == info.maxSize && info.offset < mImageSizeInBytes) {
  						logi("Executing data object (Op Code = 4)");
  						writeExecute();
  						mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");
  					} else {
  						resumeSendingData = true;
  					}
  				} else {
  					logi(info.offset + " bytes sent before, CRC does not match");
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
  							info.offset + " bytes sent before, CRC does not match");
  					// The CRC of the current object is not correct. If there was another Data object sent before, its CRC must have been correct,
  					// as it has been executed. Either way, we have to create the current object again.
  					mProgressInfo.setBytesSent(bytesSentAndExecuted);
  					mProgressInfo.setBytesReceived(bytesSentAndExecuted);
  					info.offset -= bytesSentNotExecuted;
  					info.CRC32 = 0; // invalidate
  					mFirmwareStream.reset();
  					logi("Resuming from byte " + info.offset + "...");
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
  							"Resuming from byte " + info.offset + "...");
  				}
  			} catch (final IOException e) {
  				loge("Error while reading firmware stream", e);
  				mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
  				return;
  			}
  		} else {
  			// Initialize the timer used to calculate the transfer speed
  			mProgressInfo.setBytesSent(0);
  		}
  
  		final long startTime = SystemClock.elapsedRealtime();
  
  		if (info.offset < mImageSizeInBytes) {
  			int attempt = 1;
  			// Each page will be sent in MAX_ATTEMPTS
  			while (mProgressInfo.getAvailableObjectSizeIsBytes() > 0) {
  				if (!resumeSendingData) {
  					// Create the Data object
  					final int availableObjectSizeInBytes = mProgressInfo.getAvailableObjectSizeIsBytes();
  					logi("Creating Data object (Op Code = 1, Type = 2, Size = " + availableObjectSizeInBytes + ") (" + (currentChunk + 1) + "/" + chunkCount + ")");
  					writeCreateRequest(OBJECT_DATA, availableObjectSizeInBytes);
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
                              "Data object (" + (currentChunk + 1) + "/" + chunkCount + ") created");
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
                              "Uploading firmware...");
  				} else {
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
                              "Resuming uploading firmware...");
  					resumeSendingData = false;
  				}
  
  				// Send the current object part
  				try {
  					logi("Uploading firmware...");
  					uploadFirmwareImage(mPacketCharacteristic);
  				} catch (final DeviceDisconnectedException e) {
  					loge("Disconnected while sending data");
  					throw e;
  				}
  
  				// Calculate Checksum
  				logi("Sending Calculate Checksum command (Op Code = 3)");
  				final ObjectChecksum checksum = readChecksum();
  				logi(String.format(Locale.US, "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
  				mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US,
                          "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
  
  				// It may happen, that not all bytes that were sent were received by the remote device
  				final int bytesLost = mProgressInfo.getBytesSent() - checksum.offset;
  				if (bytesLost > 0) {
  					logw(bytesLost + " bytes were lost!");
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
                              bytesLost + " bytes were lost");
  
  					try {
  						// We have to reset the stream and read 'offset' number of bytes to recalculate the CRC
  						mFirmwareStream.reset(); // Resets to the beginning of current object
  						mFirmwareStream.read(new byte[info.maxSize - bytesLost]); // Reads additional bytes that were sent and received in this object
  						mProgressInfo.setBytesSent(checksum.offset);
  					} catch (final IOException e) {
  						loge("Error while reading firmware stream", e);
  						mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
  						return;
  					}
  					// To decrease the chance of loosing data next time let's set PRN to 1. This will make the update very long, but perhaps it will succeed.
  					numberOfPacketsBeforeNotification = mPacketsBeforeNotification = 1;
  					setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
                              "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
  				}
  
  				// Calculate the CRC32
  				final int crc = (int) (((ArchiveInputStream) mFirmwareStream).getCrc32() & 0xFFFFFFFFL);
  				if (crc == checksum.CRC32) {
  					if (bytesLost > 0) {
  						resumeSendingData = true;
  						continue;
  					}
  					// Execute Init packet
  					logi("Executing data object (Op Code = 4)");
  					writeExecute(mProgressInfo.isComplete());
  					mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");
  
  					// Increment iterator
  					currentChunk++;
  					attempt = 1;
  					//Mark this location after completion of successful transfer.  In the event of a CRC retry on the next packet we will restart from this point.
  					mFirmwareStream.mark(0);
  				} else {
  					String crcFailMessage = String.format(Locale.US, "CRC does not match! Expected %08X but found %08X.", crc, checksum.CRC32);
  					if (attempt < MAX_ATTEMPTS) {
  						attempt++;
  						crcFailMessage += String.format(Locale.US, " Retrying...(%d/%d)", attempt, MAX_ATTEMPTS);
  						logi(crcFailMessage);
  						mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, crcFailMessage);
  						try {
  							// Reset the CRC and file pointer back to the previous mark() point after completion of the last successful packet.
  							mFirmwareStream.reset();
  							mProgressInfo.setBytesSent(((ArchiveInputStream) mFirmwareStream).getBytesRead());
  						} catch (final IOException e) {
  							loge("Error while resetting the firmware stream", e);
  							mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
  							return;
  						}
  					} else {
  						loge(crcFailMessage);
  						mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, crcFailMessage);
  						mService.terminateConnection(gatt, DfuBaseService.ERROR_CRC_ERROR);
  						return;
  					}
  				}
  			}
  		} else {
  			// Looks as if the whole file was sent correctly but has not been executed
  			logi("Executing data object (Op Code = 4)");
  			writeExecute(true);
  			mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");
  		}
  
  		final long endTime = SystemClock.elapsedRealtime();
  		logi("Transfer of " + (mProgressInfo.getBytesSent() - info.offset) + " bytes has taken " + (endTime - startTime) + " ms");
  		mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Upload completed in " + (endTime - startTime) + " ms");
  	}