From 0e73995630735bfd6def1e67a162b71c9ef70a4e Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Tue, 7 Jun 2016 14:27:18 +0200 Subject: [PATCH] Basic path is working on Android side --- dfu/build.gradle | 10 +- .../android/dfu/BaseCustomDfuImpl.java | 168 ++++++- .../nordicsemi/android/dfu/BaseDfuImpl.java | 9 +- .../android/dfu/DfuProgressInfo.java | 4 +- .../android/dfu/DfuServiceProvider.java | 4 + .../nordicsemi/android/dfu/LegacyDfuImpl.java | 114 +---- .../nordicsemi/android/dfu/SecureDfuImpl.java | 409 +++++++++++++++++- .../exception/UnknownResponseException.java | 6 +- .../nordicsemi/android/error/GattError.java | 16 +- .../android/error/LegacyDfuError.java | 19 + .../android/error/SecureDfuError.java | 60 +++ 11 files changed, 667 insertions(+), 152 deletions(-) create mode 100644 dfu/src/main/java/no/nordicsemi/android/error/SecureDfuError.java diff --git a/dfu/build.gradle b/dfu/build.gradle index 38d7cae..336bb79 100644 --- a/dfu/build.gradle +++ b/dfu/build.gradle @@ -41,11 +41,11 @@ ext { android { compileSdkVersion 23 - buildToolsVersion '23.0.3' + buildToolsVersion '23.0.3' - defaultConfig { - minSdkVersion 18 - targetSdkVersion 23 + defaultConfig { + minSdkVersion 18 + targetSdkVersion 23 versionCode 3 versionName "0.6.2" } @@ -55,8 +55,6 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - productFlavors { - } } dependencies { diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java index 4f276d8..88e17ba 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java @@ -22,8 +22,10 @@ package no.nordicsemi.android.dfu; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; @@ -34,6 +36,7 @@ import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; import no.nordicsemi.android.dfu.internal.exception.HexFileValidationException; import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; +import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /* package */ abstract class BaseCustomDfuImpl extends BaseDfuImpl { /** @@ -101,23 +104,22 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; mProgressInfo.addBytesSent(characteristic.getValue().length); mPacketsSentSinceNotification++; - // If a packet receipt notification is expected, or the last packet was sent, do nothing. There onCharacteristicChanged listener will catch either - // a packet confirmation (if there are more bytes to send) or the image received notification (it upload process was completed) final boolean notificationExpected = mPacketsBeforeNotification > 0 && mPacketsSentSinceNotification == mPacketsBeforeNotification; final boolean lastPacketTransferred = mProgressInfo.isComplete(); final boolean lastObjectPacketTransferred = mProgressInfo.isObjectComplete(); - // This flag may be true only in Secure DFU. - // In Secure DFU we usually do not get any notification after the object is completed, therefor the lock must be notified here. - if (lastObjectPacketTransferred) { + // When a Packet Receipt Notification notification is expected + // we must not call notifyLock() as the process will resume after notification is received. + if (notificationExpected) + return; + + // In Secure DFU we (usually, depends on the page size and PRN value) do not get any notification after the object is completed, + // therefor the lock must be notified here to resume the main process. + if (lastPacketTransferred || lastObjectPacketTransferred) { mFirmwareUploadInProgress = false; notifyLock(); return; } - // When a notification is expected (either a Packet Receipt Notification or one that's send after the whole image is completed) - // we must not call notifyLock as the process will resume after notification is received. - if (notificationExpected || lastPacketTransferred) - return; // When neither of them is true, send the next packet try { @@ -125,15 +127,15 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; // The writing might have been aborted (mAborted = true), an error might have occurred. // In that case stop sending. if (mAborted || mError != 0 || mRemoteErrorOccurred || mResetRequestSent) { - // notify waiting thread - synchronized (mLock) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload terminated"); - mLock.notifyAll(); - return; - } + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload terminated"); + notifyLock(); + return; } - final byte[] buffer = mBuffer; + final int available = mProgressInfo.getAvailableObjectSizeIsBytes(); + byte[] buffer = mBuffer; + if (available < 20) + buffer = new byte[available]; final int size = mFirmwareStream.read(buffer); writePacket(gatt, characteristic, buffer, size); return; @@ -170,8 +172,10 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; protected void handlePacketReceiptNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { // Secure DFU: // When PRN is set to be received after the object is complete we don't want to send anything. First the object needs to be executed. - if (!mFirmwareUploadInProgress) + if (!mFirmwareUploadInProgress) { + handleNotification(gatt, characteristic); return; + } final BluetoothGattCharacteristic packetCharacteristic = gatt.getService(getDfuServiceUUID()).getCharacteristic(getPacketCharacteristicUUID()); try { @@ -185,7 +189,19 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; return; } - final byte[] buffer = mBuffer; + final boolean lastPacketTransferred = mProgressInfo.isComplete(); + final boolean lastObjectPacketTransferred = mProgressInfo.isObjectComplete(); + + if (lastPacketTransferred || lastObjectPacketTransferred) { + mFirmwareUploadInProgress = false; + notifyLock(); + return; + } + + final int available = mProgressInfo.getAvailableObjectSizeIsBytes(); + byte[] buffer = mBuffer; + if (available < 20) + buffer = new byte[available]; final int size = mFirmwareStream.read(buffer); writePacket(gatt, packetCharacteristic, buffer, size); } catch (final HexFileValidationException e) { @@ -230,6 +246,26 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; protected abstract UUID getDfuServiceUUID(); + /** + * Wends the whole init packet stream to the given characteristic. + * @param characteristic the target characteristic + * @throws DfuException + * @throws DeviceDisconnectedException + * @throws UploadAbortedException + */ + protected void writeInitData(final BluetoothGattCharacteristic characteristic) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + try { + byte[] data = new byte[20]; + int size; + while ((size = mInitPacketStream.read(data, 0, data.length)) != -1) { + writeInitPacket(characteristic, data, size); + } + } catch (final IOException e) { + loge("Error while reading Init packet file", e); + throw new DfuException("Error while reading Init packet file", DfuBaseService.ERROR_FILE_ERROR); + } + } + /** * Writes the Init packet to the characteristic. This method is SYNCHRONOUS and wait until the {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} * will be called or the device gets disconnected. If connection state will change, or an error will occur, an exception will be thrown. @@ -241,7 +277,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; * @throws DfuException * @throws UploadAbortedException */ - protected void writeInitPacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) throws DeviceDisconnectedException, DfuException, + private void writeInitPacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) throws DeviceDisconnectedException, DfuException, UploadAbortedException { byte[] locBuffer = buffer; if (buffer.length != size) { @@ -329,7 +365,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; * @param buffer the buffer with 1-20 bytes * @param size the number of bytes from the buffer to send */ - protected void writePacket(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) { + private void writePacket(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) { byte[] locBuffer = buffer; if (size <= 0) // This should never happen return; @@ -339,6 +375,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); characteristic.setValue(locBuffer); + logi("Writing packet: " + parse(locBuffer)); // TODO remove gatt.writeCharacteristic(characteristic); // FIXME BLE buffer overflow // after writing to the device with WRITE_NO_RESPONSE property the onCharacteristicWrite callback is received immediately after writing data to a buffer. @@ -347,4 +384,95 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; // More info: this works fine on Nexus 5 (Android 4.4) (4.3 seconds) and on Samsung S4 (Android 4.3) (20 seconds) so this is a driver issue. // Nexus 4 and 7 uses Qualcomm chip, Nexus 5 and Samsung uses Broadcom chips. } + + /** + * Closes the BLE connection to the device and removes/restores bonding, if a proper flags were set in the {@link DfuServiceInitiator}. + * This method will also change the DFU state to completed or restart the service to send the second part. + * @param intent the intent used to start the DFU service. It contains all user flags in the bundle. + * @param forceRefresh true, if cache should be cleared even for a bonded device. Usually the Service Changed indication should be used for this purpose. + */ + protected void finalize(final Intent intent, final boolean forceRefresh) { + /* + * We are done with DFU. Now the service may refresh device cache and clear stored services. + * For bonded device this is required only if if doesn't support Service Changed indication. + * Android shouldn't cache services of non-bonded devices having Service Changed characteristic in their database, but it does, so... + */ + final boolean keepBond = intent.getBooleanExtra(DfuBaseService.EXTRA_KEEP_BOND, false); + mService.refreshDeviceCache(mGatt, forceRefresh || !keepBond); + + // Close the device + mService.close(mGatt); + + /* + * During the update the bonding information on the target device may have been removed. + * To create bond with the new application set the EXTRA_RESTORE_BOND extra to true. + * In case the bond information is copied to the new application the new bonding is not required. + */ + boolean alreadyWaited = false; + if (mGatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { + final boolean restoreBond = intent.getBooleanExtra(DfuBaseService.EXTRA_RESTORE_BOND, false); + if (restoreBond || !keepBond || (mFileType & DfuBaseService.TYPE_SOFT_DEVICE) > 0) { + // The bond information was lost. + removeBond(); + + // Give some time for removing the bond information. 300ms was to short, let's set it to 2 seconds just to be sure. + mService.waitFor(2000); + alreadyWaited = true; + } + + if (restoreBond && (mFileType & DfuBaseService.TYPE_APPLICATION) > 0) { + // Restore pairing when application was updated. + createBond(); + alreadyWaited = false; + } + } + + /* + * We need to send PROGRESS_COMPLETED message only when all files has been transmitted. + * In case you want to send the Soft Device and/or Bootloader and the Application, the service will be started twice: one to send SD+BL, and the + * second time to send the Application only (using the new Bootloader). In the first case we do not send PROGRESS_COMPLETED notification. + */ + if (mProgressInfo.isLastPart()) { + // Delay this event a little bit. Android needs some time to prepare for reconnection. + if (!alreadyWaited) + mService.waitFor(1400); + mProgressInfo.setProgress(DfuBaseService.PROGRESS_COMPLETED); + } else { + /* + * In case when the Soft Device has been upgraded, and the application should be send in the following connection, we have to + * make sure that we know the address the device is advertising with. Depending on the method used to start the DFU bootloader the first time + * the new Bootloader may advertise with the same address or one incremented by 1. + * When the buttonless update was used, the bootloader will use the same address as the application. The cached list of services on the Android device + * should be cleared thanks to the Service Changed characteristic (the fact that it exists if not bonded, or the Service Changed indication on bonded one). + * In case of forced DFU mode (using a button), the Bootloader does not know whether there was the Service Changed characteristic present in the list of + * application's services so it must advertise with a different address. The same situation applies when the new Soft Device was uploaded and the old + * application has been removed in this process. + * + * We could have save the fact of jumping as a parameter of the service but it ma be that some Android devices must first scan a device before connecting to it. + * It a device with the address+1 has never been detected before the service could have failed on connection. + */ + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Scanning for the DFU Bootloader..."); + final String newAddress = BootloaderScannerFactory.getScanner().searchFor(mGatt.getDevice().getAddress()); + if (newAddress != null) + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader found with address " + newAddress); + else { + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader not found. Trying the same address..."); + } + + /* + * The current service instance has uploaded the Soft Device and/or Bootloader. + * We need to start another instance that will try to send application only. + */ + logi("Starting service that will upload application"); + final Intent newIntent = new Intent(); + newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE); + newIntent.putExtra(DfuBaseService.EXTRA_FILE_MIME_TYPE, DfuBaseService.MIME_TYPE_ZIP); // ensure this is set (e.g. for scripts) + newIntent.putExtra(DfuBaseService.EXTRA_FILE_TYPE, DfuBaseService.TYPE_APPLICATION); // set the type to application only + if (newAddress != null) + newIntent.putExtra(DfuBaseService.EXTRA_DEVICE_ADDRESS, newAddress); + newIntent.putExtra(DfuBaseService.EXTRA_PART_CURRENT, mProgressInfo.getCurrentPart() + 1); + newIntent.putExtra(DfuBaseService.EXTRA_PARTS_TOTAL, mProgressInfo.getTotalParts()); + mService.startService(newIntent); + } + } } diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java index 92cc7e7..e29568f 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java @@ -23,7 +23,6 @@ package no.nordicsemi.android.dfu; import android.annotation.SuppressLint; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; @@ -97,6 +96,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; protected DfuBaseService mService; protected DfuProgressInfo mProgressInfo; protected int mImageSizeInBytes; + protected int mInitPacketSizeInBytes; private final BroadcastReceiver mBondStateBroadcastReceiver = new BroadcastReceiver() { @Override @@ -247,6 +247,13 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; mFirmwareStream = firmwareStream; mInitPacketStream = initPacketStream; int size; + try { + size = initPacketStream.available(); + } catch (final IOException e) { + size = 0; + // not possible + } + mInitPacketSizeInBytes = size; try { size = firmwareStream.available(); } catch (final IOException e) { diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuProgressInfo.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuProgressInfo.java index 3099046..b3f48e1 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuProgressInfo.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuProgressInfo.java @@ -93,7 +93,9 @@ import android.support.annotation.NonNull; } public int getAvailableObjectSizeIsBytes() { - return maxObjectSizeInBytes - (bytesSent % maxObjectSizeInBytes); + final int remainingBytes = imageSizeInBytes - bytesSent; + final int remainingChunk = maxObjectSizeInBytes - (bytesSent % maxObjectSizeInBytes); + return Math.min(remainingBytes, remainingChunk); } public int getProgress() { diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceProvider.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceProvider.java index 8a70160..c13b32b 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceProvider.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceProvider.java @@ -28,6 +28,10 @@ import android.bluetooth.BluetoothGattService; /* package */ class DfuServiceProvider { /* package */ static BaseDfuImpl getDfuImpl(final DfuBaseService service, final BluetoothGatt gatt) { + final BluetoothGattService secureService = gatt.getService(SecureDfuImpl.DFU_SERVICE_UUID); + if (secureService != null) { + return new SecureDfuImpl(service); + } final BluetoothGattService legacyService = gatt.getService(LegacyDfuImpl.DFU_SERVICE_UUID); if (legacyService != null) { return new LegacyDfuImpl(service); diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/LegacyDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/LegacyDfuImpl.java index 7e91ccb..7e874c5 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/LegacyDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/LegacyDfuImpl.java @@ -46,7 +46,13 @@ import no.nordicsemi.android.error.GattError; import no.nordicsemi.android.error.LegacyDfuError; /* package */ class LegacyDfuImpl extends BaseCustomDfuImpl { - public static final int DFU_STATUS_SUCCESS = 1; + // UUIDs used by the DFU + protected static final UUID DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L); + protected static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L); + protected static final UUID DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L); + protected static final UUID DFU_VERSION = new UUID(0x000015341212EFDEL, 0x1523785FEABCD123L); + + private static final int DFU_STATUS_SUCCESS = 1; // Operation codes and packets private static final int OP_CODE_START_DFU_KEY = 0x01; // 1 private static final int OP_CODE_INIT_DFU_PARAMS_KEY = 0x02; // 2 @@ -68,12 +74,6 @@ import no.nordicsemi.android.error.LegacyDfuError; //private static final byte[] OP_CODE_REPORT_RECEIVED_IMAGE_SIZE = new byte[] { OP_CODE_PACKET_REPORT_RECEIVED_IMAGE_SIZE_KEY }; private static final byte[] OP_CODE_PACKET_RECEIPT_NOTIF_REQ = new byte[]{OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY, 0x00, 0x00}; - // UUIDs used by the DFU - protected static final UUID DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L); - protected static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L); - protected static final UUID DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L); - protected static final UUID DFU_VERSION = new UUID(0x000015341212EFDEL, 0x1523785FEABCD123L); - private BluetoothGattCharacteristic mControlPointCharacteristic; private BluetoothGattCharacteristic mPacketCharacteristic; @@ -82,6 +82,8 @@ import no.nordicsemi.android.error.LegacyDfuError; */ private boolean mImageSizeInProgress; + private final LegacyBluetoothCallback mBluetoothCallback = new LegacyBluetoothCallback(); + protected class LegacyBluetoothCallback extends BaseCustomBluetoothCallback { @Override protected void onPacketCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { @@ -141,7 +143,7 @@ import no.nordicsemi.android.error.LegacyDfuError; @Override protected BaseCustomBluetoothCallback getGattCallback() { - return new LegacyBluetoothCallback(); + return mBluetoothCallback; } @Override @@ -518,16 +520,9 @@ import no.nordicsemi.android.error.LegacyDfuError; logi("Sending the Initialize DFU Parameters START (Op Code = 2, Value = 0)"); writeOpCode(mControlPointCharacteristic, OP_CODE_INIT_DFU_PARAMS_START); - try { - byte[] data = new byte[20]; - int size; - while ((size = mInitPacketStream.read(data, 0, data.length)) != -1) { - writeInitPacket(mPacketCharacteristic, data, size); - } - } catch (final IOException e) { - loge("Error while reading Init packet file"); - throw new DfuException("Error while reading Init packet file", DfuBaseService.ERROR_FILE_ERROR); - } + logi("Sending " + mImageSizeInBytes + " bytes of init packet"); + writeInitData(mPacketCharacteristic); + logi("Sending the Initialize DFU Parameters COMPLETE (Op Code = 2, Value = 1)"); writeOpCode(mControlPointCharacteristic, OP_CODE_INIT_DFU_PARAMS_COMPLETE); mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Initialize DFU Parameters completed"); @@ -601,86 +596,13 @@ import no.nordicsemi.android.error.LegacyDfuError; mService.waitUntilDisconnected(); mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Disconnected by the remote device"); + // We are ready with DFU, the device is disconnected, let's close it and finalize the operation. + // In the DFU version 0.5, in case the device is bonded, the target device does not send the Service Changed indication after // a jump from bootloader mode to app mode. This issue has been fixed in DFU version 0.6 (SDK 8.0). If the DFU bootloader has been // configured to preserve the bond information we do not need to enforce refreshing services, as it will notify the phone using the // Service Changed indication. - final boolean keepBond = intent.getBooleanExtra(DfuBaseService.EXTRA_KEEP_BOND, false); - mService.refreshDeviceCache(gatt, version == 5 || !keepBond); - - // Close the device - mService.close(gatt); - - // During the update the bonding information on the target device may have been removed. - // To create bond with the new application set the EXTRA_RESTORE_BOND extra to true. - // In case the bond information is copied to the new application the new bonding is not required. - boolean alreadyWaited = false; - if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { - final boolean restoreBond = intent.getBooleanExtra(DfuBaseService.EXTRA_RESTORE_BOND, false); - - if (restoreBond || !keepBond || (fileType & DfuBaseService.TYPE_SOFT_DEVICE) > 0) { - // The bond information was lost. - removeBond(); - - // Give some time for removing the bond information. 300ms was to short, let's set it to 2 seconds just to be sure. - mService.waitFor(2000); - alreadyWaited = true; - } - - if (restoreBond && (fileType & DfuBaseService.TYPE_APPLICATION) > 0) { - // Restore pairing when application was updated. - createBond(); - alreadyWaited = false; - } - } - - /* - * We need to send PROGRESS_COMPLETED message only when all files has been transmitted. - * In case you want to send the Soft Device and/or Bootloader and the Application, the service will be started twice: one to send SD+BL, and the - * second time to send the Application only (using the new Bootloader). In the first case we do not send PROGRESS_COMPLETED notification. - */ - if (mProgressInfo.isLastPart()) { - // Delay this event a little bit. Android needs some time to prepare for reconnection. - if (!alreadyWaited) - mService.waitFor(1400); - mProgressInfo.setProgress(DfuBaseService.PROGRESS_COMPLETED); - } else { - /* - * In case when the Soft Device has been upgraded, and the application should be send in the following connection, we have to - * make sure that we know the address the device is advertising with. Depending on the method used to start the DFU bootloader the first time - * the new Bootloader may advertise with the same address or one incremented by 1. - * When the buttonless update was used, the bootloader will use the same address as the application. The cached list of services on the Android device - * should be cleared thanks to the Service Changed characteristic (the fact that it exists if not bonded, or the Service Changed indication on bonded one). - * In case of forced DFU mode (using a button), the Bootloader does not know whether there was the Service Changed characteristic present in the list of - * application's services so it must advertise with a different address. The same situation applies when the new Soft Device was uploaded and the old - * application has been removed in this process. - * - * We could have save the fact of jumping as a parameter of the service but it ma be that some Android devices must first scan a device before connecting to it. - * It a device with the address+1 has never been detected before the service could have failed on connection. - */ - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Scanning for the DFU Bootloader..."); - final String newAddress = BootloaderScannerFactory.getScanner().searchFor(gatt.getDevice().getAddress()); - if (newAddress != null) - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader found with address " + newAddress); - else { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader not found. Trying the same address..."); - } - - /* - * The current service instance has uploaded the Soft Device and/or Bootloader. - * We need to start another instance that will try to send application only. - */ - logi("Starting service that will upload application"); - final Intent newIntent = new Intent(); - newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE); - newIntent.putExtra(DfuBaseService.EXTRA_FILE_MIME_TYPE, DfuBaseService.MIME_TYPE_ZIP); // ensure this is set (e.g. for scripts) - newIntent.putExtra(DfuBaseService.EXTRA_FILE_TYPE, DfuBaseService.TYPE_APPLICATION); // set the type to application only - if (newAddress != null) - newIntent.putExtra(DfuBaseService.EXTRA_DEVICE_ADDRESS, newAddress); - newIntent.putExtra(DfuBaseService.EXTRA_PART_CURRENT, mProgressInfo.getCurrentPart() + 1); - newIntent.putExtra(DfuBaseService.EXTRA_PARTS_TOTAL, mProgressInfo.getTotalParts()); - mService.startService(newIntent); - } + finalize(intent, version == 5); } catch (final UnknownResponseException e) { final int error = DfuBaseService.ERROR_INVALID_RESPONSE; loge(e.getMessage()); @@ -693,7 +615,7 @@ import no.nordicsemi.android.error.LegacyDfuError; } catch (final RemoteDfuException e) { final int error = DfuBaseService.ERROR_REMOTE_MASK | e.getErrorNumber(); loge(e.getMessage()); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format("Remote DFU error: %s", GattError.parse(error))); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format("Remote DFU error: %s", LegacyDfuError.parse(error))); logi("Sending Reset command (Op Code = 6)"); writeOpCode(mControlPointCharacteristic, OP_CODE_RESET); @@ -723,7 +645,7 @@ import no.nordicsemi.android.error.LegacyDfuError; */ private int getStatusCode(final byte[] response, final int request) throws UnknownResponseException { if (response == null || response.length != 3 || response[0] != OP_CODE_RESPONSE_CODE_KEY || response[1] != request || response[2] < 1 || response[2] > 6) - throw new UnknownResponseException("Invalid response received", response, request); + throw new UnknownResponseException("Invalid response received", response, OP_CODE_RESPONSE_CODE_KEY, request); return response[2]; } diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/SecureDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/SecureDfuImpl.java index 46f9869..b7186c3 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/SecureDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/SecureDfuImpl.java @@ -23,54 +23,437 @@ package no.nordicsemi.android.dfu; import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; import android.content.Intent; -import java.io.InputStream; +import java.io.IOException; +import java.util.Locale; import java.util.UUID; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; +import no.nordicsemi.android.dfu.internal.exception.RemoteDfuException; +import no.nordicsemi.android.dfu.internal.exception.SizeValidationException; +import no.nordicsemi.android.dfu.internal.exception.UnknownResponseException; import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; +import no.nordicsemi.android.error.GattError; +import no.nordicsemi.android.error.SecureDfuError; /* package */ class SecureDfuImpl extends BaseCustomDfuImpl { + // UUIDs used by the DFU + protected static final UUID DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L); // TODO should be changed later to final UUIDs + protected static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L); + protected static final UUID DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L); + + private static final int DFU_STATUS_SUCCESS = 1; + + // Object types + private static final int OBJECT_COMMAND = 0x01; + private static final int OBJECT_DATA = 0x02; + // Operation codes and packets + private static final int OP_CODE_CREATE_KEY = 0x01; + private static final int OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY = 0x02; + private static final int OP_CODE_CALCULATE_CHECKSUM_KEY = 0x03; + private static final int OP_CODE_EXECUTE_KEY = 0x04; + private static final int OP_CODE_READ_OBJECT_KEY = 0x05; + private static final int OP_CODE_READ_OBJECT_INFO_KEY = 0x06; + private static final int OP_CODE_RESPONSE_CODE_KEY = 0x60; + private static final byte[] OP_CODE_CREATE_COMMAND = new byte[]{OP_CODE_CREATE_KEY, OBJECT_COMMAND, 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] OP_CODE_CREATE_DATA = new byte[]{OP_CODE_CREATE_KEY, OBJECT_DATA, 0x00, 0x00, 0x00, 0x00}; + private static final byte[] OP_CODE_PACKET_RECEIPT_NOTIF_REQ = new byte[]{OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY, 0x00, 0x00 /* param PRN uint16 in Little Endian */}; + private static final byte[] OP_CODE_CALCULATE_CHECKSUM = new byte[]{OP_CODE_CALCULATE_CHECKSUM_KEY}; + private static final byte[] OP_CODE_EXECUTE = new byte[]{OP_CODE_EXECUTE_KEY}; + private static final byte[] OP_CODE_READ_COMMAND_OBJECT = new byte[]{OP_CODE_READ_OBJECT_KEY, OBJECT_COMMAND}; + private static final byte[] OP_CODE_READ_DATA_OBJECT = new byte[]{OP_CODE_READ_OBJECT_KEY, OBJECT_DATA}; // Reads last error message + private static final byte[] OP_CODE_READ_INFO = new byte[]{OP_CODE_READ_OBJECT_INFO_KEY, 0x00 /* type */}; + + private BluetoothGattCharacteristic mControlPointCharacteristic; + private BluetoothGattCharacteristic mPacketCharacteristic; + + private final SecureBluetoothCallback mBluetoothCallback = new SecureBluetoothCallback(); + + protected class SecureBluetoothCallback extends BaseCustomBluetoothCallback { + + @Override + public void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + if (characteristic.getValue() == null || characteristic.getValue().length < 3) { + loge("Empty response: " + parse(characteristic)); + mError = DfuBaseService.ERROR_INVALID_RESPONSE; + notifyLock(); + return; + } + + final int responseType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + + // The first byte should always be the response code + if (responseType == OP_CODE_RESPONSE_CODE_KEY) { + final int requestType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1); + + switch (requestType) { + case OP_CODE_CALCULATE_CHECKSUM_KEY: { + mProgressInfo.setBytesReceived(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3)); + logi("PRN, bytes received: " + mProgressInfo.getBytesReceived()); // TODO remove + // TODO check CRC? + handlePacketReceiptNotification(gatt, characteristic); + break; + } + default: { + /* + * If the DFU target device is in invalid state (f.e. the Init Packet is required but has not been selected), the target will send DFU_STATUS_INVALID_STATE error + * for each firmware packet that was send. We are interested may ignore all but the first one. + * After obtaining a remote DFU error the OP_CODE_RESET_KEY will be sent. + */ + if (mRemoteErrorOccurred) + break; + final int status = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 2); + if (status != DFU_STATUS_SUCCESS) + mRemoteErrorOccurred = true; + + handleNotification(gatt, characteristic); + break; + } + } + } else { + loge("Invalid response: " + parse(characteristic)); + mError = DfuBaseService.ERROR_INVALID_RESPONSE; + } + notifyLock(); + } + } SecureDfuImpl(final DfuBaseService service) { super(service); } + @Override + public boolean hasRequiredService(final BluetoothGatt gatt) { + final BluetoothGattService dfuService = gatt.getService(DFU_SERVICE_UUID); + return dfuService != null; + } + + @Override + public boolean hasRequiredCharacteristics(final BluetoothGatt gatt) { + final BluetoothGattService dfuService = gatt.getService(DFU_SERVICE_UUID); + mControlPointCharacteristic = dfuService.getCharacteristic(DFU_CONTROL_POINT_UUID); + mPacketCharacteristic = dfuService.getCharacteristic(DFU_PACKET_UUID); + return mControlPointCharacteristic != null && mPacketCharacteristic != null; + } + @Override protected BaseBluetoothGattCallback getGattCallback() { - return null; + return mBluetoothCallback; } @Override protected UUID getControlPointCharacteristicUUID() { - return null; + return DFU_CONTROL_POINT_UUID; } @Override protected UUID getPacketCharacteristicUUID() { - return null; + return DFU_PACKET_UUID; } @Override protected UUID getDfuServiceUUID() { - return null; + return DFU_SERVICE_UUID; } @Override - public boolean hasRequiredService(BluetoothGatt gatt) { - return false; + public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + mProgressInfo.setProgress(DfuBaseService.PROGRESS_STARTING); + + // Add one second delay to avoid the traffic jam before the DFU mode is enabled + // Related: + // issue: https://github.com/NordicSemiconductor/Android-DFU-Library/issues/10 + // pull request: https://github.com/NordicSemiconductor/Android-DFU-Library/pull/12 + mService.waitFor(1000); + // End + + final BluetoothGatt gatt = mGatt; + + // Enable notifications + enableCCCD(mControlPointCharacteristic, NOTIFICATIONS); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Notifications enabled"); + + // Wait a second here before going further + // Related: + // pull request: https://github.com/NordicSemiconductor/Android-DFU-Library/pull/11 + mService.waitFor(1000); + // End + + try { + byte[] response; + int status; + ObjectInfo info; + ObjectChecksum checksum; + + logi("Sending Read Command Object Info command (Op Code = 6, Type = 1)"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Reading command object info..."); + info = readObjectInfo(OBJECT_COMMAND); + if (mInitPacketSizeInBytes > info.maxSize) { + // ignore this. DFU target will send an error if init packet is too large after sending Create command + } + + logi("Disabling Packet Receipt Notifications (Op Code = 2, Value = 0)"); + setNumberOfPackets(OP_CODE_PACKET_RECEIPT_NOTIF_REQ, 0); + writeOpCode(mControlPointCharacteristic, OP_CODE_PACKET_RECEIPT_NOTIF_REQ); + + // Read response + response = readNotificationResponse(); + status = getStatusCode(response, OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Disabling Packet Receipt Notif failed", status); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Packet Receipt Notif disabled (Op Code = 2, Value = 0)"); + + // TODO resume uploading + + // Create the Init object + logi("Creating Init packet object (Op Code = 1, Type = 1, Size = " + mInitPacketSizeInBytes + ")"); + writeCreateRequest(OBJECT_COMMAND, mInitPacketSizeInBytes); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object created"); + + // Write Init data to the Packet Characteristic + logi("Sending " + mInitPacketSizeInBytes + " bytes of init packet"); + writeInitData(mPacketCharacteristic); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object sent"); + + // Calculate Checksum + logi("Sending Calculate Checksum command (Op Code = 3)"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Calculating checksum..."); + checksum = readChecksum(); + + // Execute Init packet + logi("Executing init packet (Op Code = 4)"); + writeOpCode(mControlPointCharacteristic, OP_CODE_EXECUTE); + response = readNotificationResponse(); + status = getStatusCode(response, OP_CODE_EXECUTE_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Executing Init packet failed", status); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object executed"); + + // Send the number of packets of firmware before receiving a receipt notification + final int numberOfPacketsBeforeNotification = mPacketsBeforeNotification; + if (numberOfPacketsBeforeNotification > 0) { + logi("Sending the number of packets before notifications (Op Code = 2, Value = " + numberOfPacketsBeforeNotification + ")"); + setNumberOfPackets(OP_CODE_PACKET_RECEIPT_NOTIF_REQ, numberOfPacketsBeforeNotification); + writeOpCode(mControlPointCharacteristic, OP_CODE_PACKET_RECEIPT_NOTIF_REQ); + + // Read response + response = readNotificationResponse(); + status = getStatusCode(response, OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Sending the number of packets failed", status); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")"); + } + + logi("Sending Read Data Object Info command (Op Code = 6, Type = 2)"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Reading data object info..."); + info = readObjectInfo(OBJECT_DATA); + mProgressInfo.setMaxObjectSizeInBytes(info.maxSize); + + // TODO resume? + + // Number of chunks in which the data will be sent + final int count = (mImageSizeInBytes + info.maxSize - 1) / info.maxSize; + // Chunk iterator + int i = 1; + while (mProgressInfo.getAvailableObjectSizeIsBytes() > 0) { + // Create the Data object + logi("Creating Data object (Op Code = 1, Type = 2, Size = " + mProgressInfo.getAvailableObjectSizeIsBytes() + ") (" + i + "/" + count + ")"); + writeCreateRequest(OBJECT_DATA, mProgressInfo.getAvailableObjectSizeIsBytes()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object (" + i + "/" + count + ") created"); + i++; + + // Send the current object part + uploadFirmwareImage(mPacketCharacteristic); + + // Calculate Checksum + logi("Sending Calculate Checksum command (Op Code = 3)"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Calculating checksum..."); + checksum = readChecksum(); + + // Execute Init packet + logi("Executing data object (Op Code = 4)"); + writeOpCode(mControlPointCharacteristic, OP_CODE_EXECUTE); + response = readNotificationResponse(); + status = getStatusCode(response, OP_CODE_EXECUTE_KEY); + if (status != DFU_STATUS_SUCCESS) { + // TODO read Error code + throw new RemoteDfuException("Executing data object failed", status); + } + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed"); + } + + // The device will reset so we don't have to send Disconnect signal. + mProgressInfo.setProgress(DfuBaseService.PROGRESS_DISCONNECTING); + mService.waitUntilDisconnected(); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Disconnected by the remote device"); + + // We are ready with DFU, the device is disconnected, let's close it and finalize the operation. + finalize(intent, false); + } catch (final UnknownResponseException e) { + final int error = DfuBaseService.ERROR_INVALID_RESPONSE; + loge(e.getMessage()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, e.getMessage()); + +// logi("Sending Reset command (Op Code = 6)"); +// writeOpCode(mControlPointCharacteristic, OP_CODE_RESET); +// mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Reset request sent"); + mService.terminateConnection(gatt, error); + } catch (final RemoteDfuException e) { + // TODO read Error code + + final int error = DfuBaseService.ERROR_REMOTE_MASK | e.getErrorNumber(); + loge(e.getMessage()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format("Remote DFU error: %s", SecureDfuError.parse(error))); + +// logi("Sending Reset command (Op Code = 6)"); +// writeOpCode(mControlPointCharacteristic, OP_CODE_RESET); +// mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Reset request sent"); + mService.terminateConnection(gatt, error); + } } - @Override - public boolean hasRequiredCharacteristics(BluetoothGatt gatt) { - return false; + /** + * Checks whether the response received is valid and returns the status code. + * + * @param response the response received from the DFU device. + * @param request the expected Op Code + * @return the status code + * @throws UnknownResponseException if response was not valid + */ + private int getStatusCode(final byte[] response, final int request) throws UnknownResponseException { + if (response == null || response.length < 3 || response[0] != OP_CODE_RESPONSE_CODE_KEY || response[1] != request || response[2] < SecureDfuError.INVALID_CODE || response[2] > 10) + throw new UnknownResponseException("Invalid response received", response, OP_CODE_RESPONSE_CODE_KEY, request); + return response[2]; } - @Override - public void performDfu(Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + /** + * Sets number of data packets that will be send before the notification will be received. + * + * @param data control point data packet + * @param value number of packets before receiving notification. If this value is 0, then the notification of packet receipt will be disabled by the DFU target. + */ + private void setNumberOfPackets(final byte[] data, final int value) { + data[1] = (byte) (value & 0xFF); + data[2] = (byte) ((value >> 8) & 0xFF); + } + /** + * Sets the object size in correct position of the data array. + * + * @param data control point data packet + * @param value Object size in bytes. + */ + private void setObjectSize(final byte[] data, final int value) { + data[2] = (byte) (value & 0xFF); + data[3] = (byte) ((value >> 8) & 0xFF); + data[4] = (byte) ((value >> 16) & 0xFF); + data[5] = (byte) ((value >> 24) & 0xFF); + } + + /** + * Writes the operation code to the characteristic. This method is SYNCHRONOUS and wait until the + * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} + * will be called or the device gets disconnected. + * If connection state will change, or an error will occur, an exception will be thrown. + * + * @param characteristic the characteristic to write to. Should be the DFU CONTROL POINT + * @param value the value to write to the characteristic + * @throws DeviceDisconnectedException + * @throws DfuException + * @throws UploadAbortedException + */ + private void writeOpCode(final BluetoothGattCharacteristic characteristic, final byte[] value) throws DeviceDisconnectedException, DfuException, UploadAbortedException { + writeOpCode(characteristic, value, false); + } + + /** + * Writes Create Object request providing the type and size of the object. + * @param type {@link #OBJECT_COMMAND} or {@link #OBJECT_DATA} + * @param size size of the object or current part of the object + * @throws DeviceDisconnectedException + * @throws DfuException + * @throws UploadAbortedException + * @throws RemoteDfuException + * @throws UnknownResponseException + */ + private void writeCreateRequest(final int type, final int size) throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException { + if (!mConnected) + throw new DeviceDisconnectedException("Unable to create object: device disconnected"); + + final byte[] data = (type == OBJECT_COMMAND) ? OP_CODE_CREATE_COMMAND : OP_CODE_CREATE_DATA; + setObjectSize(data, size); + writeOpCode(mControlPointCharacteristic, data); + + final byte[] response = readNotificationResponse(); + final int status = getStatusCode(response, OP_CODE_CREATE_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Creating Command object failed", status); + } + + /** + * Reads the Object Info for object with given type. The object info contains the max object size, the last offset and CRC32 of the whole object until now. + * + * @return requested object info + * @throws DeviceDisconnectedException + * @throws DfuException + * @throws UploadAbortedException + */ + private ObjectInfo readObjectInfo(final int type) throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException { + if (!mConnected) + throw new DeviceDisconnectedException("Unable to read object info: device disconnected"); + + OP_CODE_READ_INFO[1] = (byte) type; + writeOpCode(mControlPointCharacteristic, OP_CODE_READ_INFO); + + final byte[] response = readNotificationResponse(); + final int status = getStatusCode(response, OP_CODE_READ_OBJECT_INFO_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Reading Object Info failed", status); + + final ObjectInfo info = new ObjectInfo(); + info.maxSize = mControlPointCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3); + info.offset = mControlPointCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 4); + info.CRC32 = mControlPointCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 8); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Response received (Op Code = %d, Status = %d, Max size = %d, Offset = %d, CRC = %08X)", response[1], status, info.maxSize, info.offset, info.CRC32)); + return info; + } + + /** + * Sends the Calculate Checksum request. As a response a notification will be sent with current offset and CRC32 of the current object. + * + * @return requested object info + * @throws DeviceDisconnectedException + * @throws DfuException + * @throws UploadAbortedException + */ + private ObjectChecksum readChecksum() throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException { + if (!mConnected) + throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected"); + + writeOpCode(mControlPointCharacteristic, OP_CODE_CALCULATE_CHECKSUM); + + final byte[] response = readNotificationResponse(); + final int status = getStatusCode(response, OP_CODE_CALCULATE_CHECKSUM_KEY); + if (status != DFU_STATUS_SUCCESS) + throw new RemoteDfuException("Receiving Checksum failed", status); + + final ObjectChecksum info = new ObjectChecksum(); + info.offset = mControlPointCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3); + info.CRC32 = mControlPointCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 4); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Response received (Op Code = %d, Status = %d, Offset = %d, CRC = %08X)", response[1], status, info.offset, info.CRC32)); + return info; + } + + private class ObjectInfo extends ObjectChecksum { + protected int maxSize; + } + + private class ObjectChecksum { + protected int offset; + protected int CRC32; } } diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/internal/exception/UnknownResponseException.java b/dfu/src/main/java/no/nordicsemi/android/dfu/internal/exception/UnknownResponseException.java index 5e19107..e3ab6e7 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/internal/exception/UnknownResponseException.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/internal/exception/UnknownResponseException.java @@ -27,18 +27,20 @@ public class UnknownResponseException extends Exception { private static final char[] HEX_ARRAY = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private final byte[] mResponse; + private final int mExpectedReturnCode; private final int mExpectedOpCode; - public UnknownResponseException(final String message, final byte[] response, final int expectedOpCode) { + public UnknownResponseException(final String message, final byte[] response, final int expectedReturnCode, final int expectedOpCode) { super(message); mResponse = response != null ? response : new byte[0]; + mExpectedReturnCode = expectedReturnCode; mExpectedOpCode = expectedOpCode; } @Override public String getMessage() { - return String.format("%s (response: %s, expected: 0x10%02X..)", super.getMessage(), bytesToHex(mResponse, 0, mResponse.length), mExpectedOpCode); + return String.format("%s (response: %s, expected: 0x%02X%02X..)", super.getMessage(), bytesToHex(mResponse, 0, mResponse.length), mExpectedReturnCode, mExpectedOpCode); } public static String bytesToHex(final byte[] bytes, final int start, final int length) { diff --git a/dfu/src/main/java/no/nordicsemi/android/error/GattError.java b/dfu/src/main/java/no/nordicsemi/android/error/GattError.java index be9bd4d..72a8791 100644 --- a/dfu/src/main/java/no/nordicsemi/android/error/GattError.java +++ b/dfu/src/main/java/no/nordicsemi/android/error/GattError.java @@ -169,21 +169,11 @@ public class GattError { case DfuBaseService.ERROR_INIT_PACKET_REQUIRED: return "INIT PACKET REQUIRED"; default: + // Deprecated: use Legacy or SecureDfuError parser if ((DfuBaseService.ERROR_REMOTE_MASK & error) > 0) { - switch (error & (~DfuBaseService.ERROR_REMOTE_MASK)) { - case LegacyDfuError.INVALID_STATE: - return "REMOTE DFU INVALID STATE"; - case LegacyDfuError.NOT_SUPPORTED: - return "REMOTE DFU NOT SUPPORTED"; - case LegacyDfuError.DATA_SIZE_EXCEEDS_LIMIT: - return "REMOTE DFU DATA SIZE EXCEEDS LIMIT"; - case LegacyDfuError.CRC_ERROR: - return "REMOTE DFU INVALID CRC ERROR"; - case LegacyDfuError.OPERATION_FAILED: - return "REMOTE DFU OPERATION FAILED"; - } + return LegacyDfuError.parse(error); } - return "UNKNOWN (" + error + ")"; } + return "UNKNOWN (" + error + ")"; } } diff --git a/dfu/src/main/java/no/nordicsemi/android/error/LegacyDfuError.java b/dfu/src/main/java/no/nordicsemi/android/error/LegacyDfuError.java index cf03220..fce74c5 100644 --- a/dfu/src/main/java/no/nordicsemi/android/error/LegacyDfuError.java +++ b/dfu/src/main/java/no/nordicsemi/android/error/LegacyDfuError.java @@ -22,6 +22,8 @@ package no.nordicsemi.android.error; +import no.nordicsemi.android.dfu.DfuBaseService; + public final class LegacyDfuError { // DFU status values public static final int INVALID_STATE = 2; @@ -29,4 +31,21 @@ public final class LegacyDfuError { public static final int DATA_SIZE_EXCEEDS_LIMIT = 4; public static final int CRC_ERROR = 5; public static final int OPERATION_FAILED = 6; + + public static String parse(final int error) { + switch (error & (~DfuBaseService.ERROR_REMOTE_MASK)) { + case INVALID_STATE: + return "REMOTE DFU INVALID STATE"; + case NOT_SUPPORTED: + return "REMOTE DFU NOT SUPPORTED"; + case DATA_SIZE_EXCEEDS_LIMIT: + return "REMOTE DFU DATA SIZE EXCEEDS LIMIT"; + case CRC_ERROR: + return "REMOTE DFU INVALID CRC ERROR"; + case OPERATION_FAILED: + return "REMOTE DFU OPERATION FAILED"; + default: + return "UNKNOWN (" + error + ")"; + } + } } diff --git a/dfu/src/main/java/no/nordicsemi/android/error/SecureDfuError.java b/dfu/src/main/java/no/nordicsemi/android/error/SecureDfuError.java new file mode 100644 index 0000000..8bf2845 --- /dev/null +++ b/dfu/src/main/java/no/nordicsemi/android/error/SecureDfuError.java @@ -0,0 +1,60 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2016, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ + +package no.nordicsemi.android.error; + +import no.nordicsemi.android.dfu.DfuBaseService; + +public final class SecureDfuError { + // DFU status values + public static final int INVALID_CODE = 0; + public static final int OP_CODE_NOT_SUPPORTED = 2; + public static final int INVALID_PARAM = 3; + public static final int INSUFFICIENT_RESOURCES = 4; + public static final int INVALID_OBJECT = 5; + public static final int SIGNATURE_DOES_NOT_MATCH = 6; + public static final int UNSUPPORTED_TYPE = 7; + public static final int OPERATION_FAILED = 10; // 0xA + + public static String parse(final int error) { + switch (error & (~DfuBaseService.ERROR_REMOTE_MASK)) { + case INVALID_CODE: + return "REMOTE DFU INVALID CODE"; + case OP_CODE_NOT_SUPPORTED: + return "REMOTE DFU OP CODE NOT SUPPORTED"; + case INVALID_PARAM: + return "REMOTE DFU INVALID PARAM"; + case INSUFFICIENT_RESOURCES: + return "REMOTE DFU INSUFFICIENT RESOURCES"; + case INVALID_OBJECT: + return "REMOTE DFU INVALID OBJECT"; + case SIGNATURE_DOES_NOT_MATCH: + return "REMOTE DFU SIGNATURE DOES NOT MATCH"; + case UNSUPPORTED_TYPE: + return "REMOTE DFU UNSUPPORTED TYPE"; + case OPERATION_FAILED: + return "REMOTE DFU OPERATION FAILED"; + default: + return "UNKNOWN (" + error + ")"; + } + } +}