Initial changes for Legacy DFU, not working.

This commit is contained in:
Aleksander Nowakowski 2016-05-25 15:59:43 +02:00
parent e97e52eee3
commit c901011d04
10 changed files with 1721 additions and 1306 deletions

View File

@ -41,8 +41,7 @@ ext {
android { android {
compileSdkVersion 23 compileSdkVersion 23
buildToolsVersion '23.0.2' buildToolsVersion '23.0.3'
defaultConfig { defaultConfig {
minSdkVersion 18 minSdkVersion 18
targetSdkVersion 23 targetSdkVersion 23
@ -55,11 +54,13 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
productFlavors {
}
} }
dependencies { dependencies {
compile fileTree(dir: 'libs', include: ['*.jar']) compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:support-v4:23.3.0'
compile 'com.google.code.gson:gson:2.5' compile 'com.google.code.gson:gson:2.5'
} }
/* /*

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":dfu" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="DFULibrary" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.id=":dfu" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../nRFMasterControlPanel" external.system.id="GRADLE" external.system.module.group="nRFMasterControlPanel" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle"> <facet type="android-gradle" name="Android-Gradle">
<configuration> <configuration>
@ -12,10 +12,7 @@
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" /> <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<afterSyncTasks> <afterSyncTasks>
<task>generateDebugAndroidTestSources</task>
<task>generateDebugSources</task> <task>generateDebugSources</task>
</afterSyncTasks> </afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" /> <option name="ALLOW_USER_CONFIGURATION" value="false" />
@ -29,7 +26,7 @@
</component> </component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false"> <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" /> <output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
@ -51,6 +48,15 @@
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
@ -58,6 +64,15 @@
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
@ -65,34 +80,30 @@
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/docs" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotations" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotations" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.1.1/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.3.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/ivy.xml" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" /> <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/poms" />
<excludeFolder url="file://$MODULE_DIR$/build/release" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content> </content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-annotations-23.1.1" level="project" /> <orderEntry type="library" exported="" name="support-v4-23.3.0" level="project" />
<orderEntry type="library" exported="" name="support-v4-23.1.1" level="project" /> <orderEntry type="library" exported="" name="support-annotations-23.3.0" level="project" />
<orderEntry type="library" exported="" name="gson-2.5" level="project" /> <orderEntry type="library" exported="" name="gson-2.5" level="project" />
</component> </component>
</module> </module>

View File

@ -0,0 +1,338 @@
/*************************************************************************************************************************************************
* 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.dfu;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import java.io.IOException;
import java.io.InputStream;
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.HexFileValidationException;
import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
/* package */ abstract class BaseCustomDfuImpl extends BaseDfuImpl {
/**
* Flag indicating whether the init packet has been already transferred or not.
*/
private boolean mInitPacketSent;
/**
* Flag indicating whether the firmware is being transmitted or not.
*/
private boolean mFirmwareUploadStarted;
/**
* The number of packets of firmware data to be send before receiving a new Packets receipt notification. 0 disables the packets notifications.
*/
protected final int mPacketsBeforeNotification;
/**
* The number of packets sent since last notification.
*/
protected int mPacketsSentSinceNotification;
/**
* <p>
* Flag set to <code>true</code> when the DFU target had send a notification with status other than success. Setting it to <code>true</code> will abort sending firmware and
* stop logging notifications (read below for explanation).
* </p>
* <p>
* The onCharacteristicWrite(..) callback is called when Android writes the packet into the outgoing queue, not when it physically sends the data.
* This means that the service will first put up to N* packets, one by one, to the queue, while in fact the first one is transmitted.
* In case the DFU target is in an invalid state it will notify Android with a notification 10-03-02 for each packet of firmware that has been sent.
* After receiving the first such notification, the DFU service will add the reset command to the outgoing queue, but it will still be receiving such notifications
* until all the data packets are sent. Those notifications should be ignored. This flag will prevent from logging "Notification received..." more than once.
* </p>
* <p>
* Additionally, sometimes after writing the command 6 ({@link LegacyDfuImpl#OP_CODE_RESET}), Android will receive a notification and update the characteristic value with 10-03-02 and the callback for write
* reset command will log "[DFU] Data written to ..., value (0x): 10-03-02" instead of "...(x0): 06". But this does not matter for the DFU process.
* </p>
* <p>
* N* - Value of Packet Receipt Notification, 10 by default.
* </p>
*/
protected boolean mRemoteErrorOccurred;
protected class BaseCustomBluetoothCallback extends BaseBluetoothGattCallback {
protected void onPacketCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
// this method must be overwritten on the final class
}
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
/*
* This method is called when either a CONTROL POINT or PACKET characteristic has been written.
* If it is the CONTROL POINT characteristic, just set the {@link mRequestCompleted} flag to true. The main thread will continue its task when notified.
* If the PACKET characteristic was written we must:
* - if the image size was written in DFU Start procedure, just set flag to true
* otherwise
* - send the next packet, if notification is not required at that moment, or
* - do nothing, because we have to wait for the notification to confirm the data received
*/
if (characteristic.getUuid().equals(getPacketCharacteristicUUID())) {
if (mInitPacketSent) {
// We've got confirmation that the init packet was sent
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic));
mInitPacketSent = false;
} else if (mFirmwareUploadStarted) {
// If the PACKET characteristic was written with image data, update counters
mProgressInfo.addBytesSent(characteristic.getValue().length);
mService.updateProgressNotification(mProgressInfo);
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) {
mFirmwareUploadStarted = 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 {
waitIfPaused();
// 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;
}
}
final byte[] buffer = mBuffer;
final int size = mFirmwareStream.read(buffer);
writePacket(gatt, characteristic, buffer, size);
return;
} catch (final HexFileValidationException e) {
loge("Invalid HEX file");
mError = DfuBaseService.ERROR_FILE_INVALID;
} catch (final IOException e) {
loge("Error while reading the input stream", e);
mError = DfuBaseService.ERROR_FILE_IO_EXCEPTION;
}
} else {
onPacketCharacteristicWrite(gatt, characteristic, status);
}
} else {
// If the CONTROL POINT characteristic was written just set the flag to true. The main thread will continue its task when notified.
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic));
mRequestCompleted = true;
}
} else {
/*
* If a Reset (Op Code = 6) or Activate and Reset (Op Code = 5) commands are sent, the DFU target resets and sometimes does it so quickly that does not manage to send
* any ACK to the controller and error 133 is thrown here. This bug should be fixed in SDK 8.0+ where the target would gracefully disconnect before restarting.
*/
if (mResetRequestSent)
mRequestCompleted = true;
else {
loge("Characteristic write error: " + status);
mError = DfuBaseService.ERROR_CONNECTION_MASK | status;
}
}
notifyLock();
}
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 (!mFirmwareUploadStarted)
return;
final BluetoothGattCharacteristic packetCharacteristic = gatt.getService(getDfuServiceUUID()).getCharacteristic(getPacketCharacteristicUUID());
try {
mPacketsSentSinceNotification = 0;
waitIfPaused();
// The writing might have been aborted (mAborted = true), an error might have occurred.
// In that case quit sending.
if (mAborted || mError != 0 || mRemoteErrorOccurred || mResetRequestSent) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload terminated");
return;
}
final byte[] buffer = mBuffer;
final int size = mFirmwareStream.read(buffer);
writePacket(gatt, packetCharacteristic, buffer, size);
} catch (final HexFileValidationException e) {
loge("Invalid HEX file");
mError = DfuBaseService.ERROR_FILE_INVALID;
} catch (final IOException e) {
loge("Error while reading the input stream", e);
mError = DfuBaseService.ERROR_FILE_IO_EXCEPTION;
}
}
protected void handleNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Notification received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic));
mReceivedData = characteristic.getValue();
mFirmwareUploadStarted = false;
}
}
BaseCustomDfuImpl(final DfuBaseService service, final InputStream firmwareStream, final InputStream initPacketStream, final int packetsBeforeNotification) {
super(service, firmwareStream, initPacketStream);
mPacketsBeforeNotification = packetsBeforeNotification;
}
protected abstract UUID getControlPointCharacteristicUUID();
protected abstract UUID getPacketCharacteristicUUID();
protected abstract UUID getDfuServiceUUID();
/**
* 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 connection state will change from {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device
* @param characteristic the characteristic to write to. Should be the DFU PACKET
* @param buffer the init packet as a byte array. This must be shorter or equal to 20 bytes (TODO check this restriction).
* @param size the init packet size
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
protected void writeInitPacket(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) throws DeviceDisconnectedException, DfuException,
UploadAbortedException {
byte[] locBuffer = buffer;
if (buffer.length != size) {
locBuffer = new byte[size];
System.arraycopy(buffer, 0, locBuffer, 0, size);
}
mReceivedData = null;
mError = 0;
mInitPacketSent = true;
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
characteristic.setValue(locBuffer);
logi("Sending init packet (Value = " + parse(locBuffer) + ")");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Writing to characteristic " + characteristic.getUuid());
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
gatt.writeCharacteristic(characteristic);
// We have to wait for confirmation
try {
synchronized (mLock) {
while ((mInitPacketSent && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Init DFU Parameters", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to write Init DFU Parameters", mConnectionState);
}
/**
* Starts sending the data. This method is SYNCHRONOUS and terminates when the whole file will be uploaded or the connection status will change from {@link #STATE_CONNECTED_AND_READY}. If
* connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device (DFU target)
* @param packetCharacteristic the characteristic to write file content to. Must be the DFU PACKET
* @return The response value received from notification with Op Code = 3 when all bytes will be uploaded successfully.
* @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of the transmission. The error core will be saved in {@link #mConnectionState}.
* @throws DfuException Thrown if DFU error occur
* @throws UploadAbortedException
*/
protected byte[] uploadFirmwareImage(final BluetoothGatt gatt, final BluetoothGattCharacteristic packetCharacteristic) throws DeviceDisconnectedException,
DfuException, UploadAbortedException {
mReceivedData = null;
mError = 0;
mFirmwareUploadStarted = true;
final byte[] buffer = mBuffer;
try {
final int size = mFirmwareStream.read(buffer);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Sending firmware to characteristic " + packetCharacteristic.getUuid() + "...");
writePacket(gatt, packetCharacteristic, buffer, size);
} catch (final HexFileValidationException e) {
throw new DfuException("HEX file not valid", DfuBaseService.ERROR_FILE_INVALID);
} catch (final IOException e) {
throw new DfuException("Error while reading file", DfuBaseService.ERROR_FILE_IO_EXCEPTION);
}
try {
synchronized (mLock) {
while ((mFirmwareUploadStarted && mReceivedData == null && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Uploading Firmware Image failed", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Uploading Firmware Image failed: device disconnected", mConnectionState);
return mReceivedData;
}
/**
* Writes the buffer to the characteristic. The maximum size of the buffer is 20 bytes. This method is ASYNCHRONOUS and returns immediately after adding the data to TX queue.
*
* @param gatt the GATT device
* @param characteristic the characteristic to write to. Should be the DFU PACKET
* @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) {
byte[] locBuffer = buffer;
if (size <= 0) // This should never happen
return;
if (buffer.length != size) {
locBuffer = new byte[size];
System.arraycopy(buffer, 0, locBuffer, 0, size);
}
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
characteristic.setValue(locBuffer);
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.
// The real sending is much slower than adding to the buffer. This method does not return false if writing didn't succeed.. just the callback is not invoked.
//
// 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.
}
}

View File

@ -0,0 +1,802 @@
/*************************************************************************************************************************************************
* 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.dfu;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
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.HexFileValidationException;
import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
/* package */ abstract class BaseDfuImpl {
public static final String TAG = "DfuImpl";
protected static final UUID GENERIC_ATTRIBUTE_SERVICE_UUID = new UUID(0x0000180100001000L, 0x800000805F9B34FBL);
protected static final UUID SERVICE_CHANGED_UUID = new UUID(0x00002A0500001000L, 0x800000805F9B34FBL);
protected static final UUID CLIENT_CHARACTERISTIC_CONFIG = new UUID(0x0000290200001000L, 0x800000805f9b34fbL);
private static final int NOTIFICATIONS = 1;
private static final int INDICATIONS = 2;
protected static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
protected static final int MAX_PACKET_SIZE = 20; // the maximum number of bytes in one packet is 20. May be less.
/**
* The current connection state. If its value is > 0 than an error has occurred. Error number is a negative value of mConnectionState
*/
protected int mConnectionState;
protected final static int STATE_DISCONNECTED = 0;
protected final static int STATE_CONNECTING = -1;
protected final static int STATE_CONNECTED = -2;
protected final static int STATE_CONNECTED_AND_READY = -3; // indicates that services were discovered
protected final static int STATE_DISCONNECTING = -4;
protected final static int STATE_CLOSED = -5;
/**
* Lock used in synchronization purposes
*/
protected final Object mLock = new Object();
protected final InputStream mFirmwareStream;
protected final InputStream mInitPacketStream;
/** Flag set to true if sending was paused. */
protected boolean mPaused;
/** Flag set to true if sending was aborted. */
protected boolean mAborted;
/**
* Flag indicating whether the request was completed or not
*/
protected boolean mRequestCompleted;
/**
* Flag sent when a request has been sent that will cause the DFU target to reset. Often, after sending such command, Android throws a connection state error. If this flag is set the error will be
* ignored.
*/
protected boolean mResetRequestSent;
/**
* The number of the last error that has occurred or 0 if there was no error
*/
protected int mError;
/**
* Latest data received from device using notification.
*/
protected byte[] mReceivedData = null;
protected final byte[] mBuffer = new byte[MAX_PACKET_SIZE];
/**
* The target device address
*/
private String mDeviceAddress;
private BluetoothAdapter mBluetoothAdapter;
protected DfuBaseService mService;
protected final DfuProgressInfo mProgressInfo;
private final BroadcastReceiver mConnectionStateBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
// Obtain the device and check it this is the one that we are connected to
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (!device.getAddress().equals(mDeviceAddress))
return;
final String action = intent.getAction();
logi("Action received: " + action);
mConnectionState = STATE_DISCONNECTED;
notifyLock();
}
};
private final BroadcastReceiver mBondStateBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
// Obtain the device and check it this is the one that we are connected to
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (!device.getAddress().equals(mDeviceAddress))
return;
// Read bond state
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
if (bondState == BluetoothDevice.BOND_BONDING)
return;
mRequestCompleted = true;
notifyLock();
}
};
protected class BaseBluetoothGattCallback extends BluetoothGattCallback {
@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
// Check whether an error occurred
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
logi("Connected to GATT server");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Connected to " + mDeviceAddress);
mConnectionState = STATE_CONNECTED;
/*
* The onConnectionStateChange callback is called just after establishing connection and before sending Encryption Request BLE event in case of a paired device.
* In that case and when the Service Changed CCCD is enabled we will get the indication after initializing the encryption, about 1600 milliseconds later.
* If we discover services right after connecting, the onServicesDiscovered callback will be called immediately, before receiving the indication and the following
* service discovery and we may end up with old, application's services instead.
*
* This is to support the buttonless switch from application to bootloader mode where the DFU bootloader notifies the master about service change.
* Tested on Nexus 4 (Android 4.4.4 and 5), Nexus 5 (Android 5), Samsung Note 2 (Android 4.4.2). The time after connection to end of service discovery is about 1.6s
* on Samsung Note 2.
*
* NOTE: We are doing this to avoid the hack with calling the hidden gatt.refresh() method, at least for bonded devices.
*/
if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
try {
synchronized (this) {
logd("Waiting 1600 ms for a possible Service Changed indication...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "wait(1600)");
wait(1600);
// After 1.6s the services are already discovered so the following gatt.discoverServices() finishes almost immediately.
// NOTE: This also works with shorted waiting time. The gatt.discoverServices() must be called after the indication is received which is
// about 600ms after establishing connection. Values 600 - 1600ms should be OK.
}
} catch (final InterruptedException e) {
// Do nothing
}
}
// Attempts to discover services after successful connection.
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Discovering services...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.discoverServices()");
final boolean success = gatt.discoverServices();
logi("Attempting to start service discovery... " + (success ? "succeed" : "failed"));
if (!success) {
mError = DfuBaseService.ERROR_SERVICE_DISCOVERY_NOT_STARTED;
} else {
// Just return here, lock will be notified when service discovery finishes
return;
}
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
logi("Disconnected from GATT server");
mPaused = false;
mConnectionState = STATE_DISCONNECTED;
}
} else {
loge("Connection state change error: " + status + " newState: " + newState);
if (newState == BluetoothGatt.STATE_DISCONNECTED)
mConnectionState = STATE_DISCONNECTED;
mPaused = false;
mError = DfuBaseService.ERROR_CONNECTION_STATE_MASK | status;
}
notifyLock();
}
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
logi("Services discovered");
mConnectionState = STATE_CONNECTED_AND_READY;
} else {
loge("Service discovery error: " + status);
mError = DfuBaseService.ERROR_CONNECTION_MASK | status;
}
notifyLock();
}
@Override
public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
/*
* This method is called when the DFU Version characteristic has been read.
*/
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Read Response received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic));
mReceivedData = characteristic.getValue();
mRequestCompleted = true;
} else {
loge("Characteristic read error: " + status);
mError = DfuBaseService.ERROR_CONNECTION_MASK | status;
}
notifyLock();
}
@Override
public void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (CLIENT_CHARACTERISTIC_CONFIG.equals(descriptor.getUuid())) {
if (SERVICE_CHANGED_UUID.equals(descriptor.getCharacteristic().getUuid())) {
// We have enabled indications for the Service Changed characteristic
mRequestCompleted = true;
} else {
// reading other descriptor is not supported
loge("Unknown descriptor read"); // this have to be implemented if needed
}
}
} else {
loge("Descriptor read error: " + status);
mError = DfuBaseService.ERROR_CONNECTION_MASK | status;
}
notifyLock();
}
@Override
public void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (CLIENT_CHARACTERISTIC_CONFIG.equals(descriptor.getUuid())) {
if (SERVICE_CHANGED_UUID.equals(descriptor.getCharacteristic().getUuid())) {
// We have enabled indications for the Service Changed characteristic
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Indications enabled for " + descriptor.getCharacteristic().getUuid());
} else {
// We have enabled notifications for this characteristic
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Notifications enabled for " + descriptor.getCharacteristic().getUuid());
}
}
} else {
loge("Descriptor write error: " + status);
mError = DfuBaseService.ERROR_CONNECTION_MASK | status;
}
notifyLock();
}
// This method is repeated here and in the service class for performance matters.
protected String parse(final BluetoothGattCharacteristic characteristic) {
final byte[] data = characteristic.getValue();
if (data == null)
return "";
final int length = data.length;
if (length == 0)
return "";
final char[] out = new char[length * 3 - 1];
for (int j = 0; j < length; j++) {
int v = data[j] & 0xFF;
out[j * 3] = HEX_ARRAY[v >>> 4];
out[j * 3 + 1] = HEX_ARRAY[v & 0x0F];
if (j != length - 1)
out[j * 3 + 2] = '-';
}
return new String(out);
}
};
/* package */ BaseDfuImpl(final DfuBaseService service, final InputStream firmwareStream, final InputStream initPacketStream) {
mService = service;
mFirmwareStream = firmwareStream;
mInitPacketStream = initPacketStream;
int size;
try {
size = firmwareStream.available();
} catch (final IOException e) {
size = 0;
// not possible
}
mProgressInfo = new DfuProgressInfo(size);
final IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);
service.registerReceiver(mConnectionStateBroadcastReceiver, filter);
final IntentFilter bondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
service.registerReceiver(mBondStateBroadcastReceiver, bondFilter);
}
/* package */ void unregister() {
mService.unregisterReceiver(mConnectionStateBroadcastReceiver);
mService.unregisterReceiver(mBondStateBroadcastReceiver);
mService = null;
}
/* package */ void pause() {
mPaused = true;
}
/* package */ void resume() {
mPaused = false;
notifyLock();
}
/* package */ void abort() {
mPaused = false;
mAborted = true;
notifyLock();
}
protected void notifyLock() {
// Notify waiting thread
synchronized (mLock) {
mLock.notifyAll();
}
}
protected void waitIfPaused() {
try {
synchronized (mLock) {
while (mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
}
/**
* Returns the final BluetoothGattCallback instance, depending on the implementation.
*/
protected abstract BluetoothGattCallback getGattCallback();
/**
* Connects to the BLE device with given address. This method is SYNCHRONOUS, it wait until the connection status change from {@link #STATE_CONNECTING} to {@link #STATE_CONNECTED_AND_READY} or an
* error occurs. This method returns <code>null</code> if Bluetooth adapter is disabled.
*
* @param address the device address
* @return the GATT device or <code>null</code> if Bluetooth adapter is disabled.
*/
protected BluetoothGatt connect(final String address) {
if (!mBluetoothAdapter.isEnabled())
return null;
mConnectionState = STATE_CONNECTING;
logi("Connecting to the device...");
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt = device.connectGatt(autoConnect = false)");
final BluetoothGatt gatt = device.connectGatt(mService, false, getGattCallback());
// We have to wait until the device is connected and services are discovered
// Connection error may occur as well.
try {
synchronized (mLock) {
while (((mConnectionState == STATE_CONNECTING || mConnectionState == STATE_CONNECTED) && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
return gatt;
}
/**
* Disconnects from the device and cleans local variables in case of error. This method is SYNCHRONOUS and wait until the disconnecting process will be completed.
*
* @param gatt the GATT device to be disconnected
* @param error error number
*/
protected void terminateConnection(final BluetoothGatt gatt, final int error) {
if (mConnectionState != STATE_DISCONNECTED) {
// Disconnect from the device
disconnect(gatt);
}
// Close the device
refreshDeviceCache(gatt, false); // This should be set to true when DFU Version is 0.5 or lower
close(gatt);
mService.updateProgressNotification(error);
}
/**
* Disconnects from the device. This is SYNCHRONOUS method and waits until the callback returns new state. Terminates immediately if device is already disconnected. Do not call this method
* directly, use {@link #terminateConnection(android.bluetooth.BluetoothGatt, int)} instead.
*
* @param gatt the GATT device that has to be disconnected
*/
protected void disconnect(final BluetoothGatt gatt) {
if (mConnectionState == STATE_DISCONNECTED)
return;
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Disconnecting...");
mService.updateProgressNotification(mProgressInfo.setProgress(DfuBaseService.PROGRESS_DISCONNECTING));
mConnectionState = STATE_DISCONNECTING;
logi("Disconnecting from the device...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.disconnect()");
gatt.disconnect();
// We have to wait until device gets disconnected or an error occur
waitUntilDisconnected();
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Disconnected");
}
/**
* Wait until the connection state will change to {@link #STATE_DISCONNECTED} or until an error occurs.
*/
protected void waitUntilDisconnected() {
try {
synchronized (mLock) {
while (mConnectionState != STATE_DISCONNECTED && mError == 0)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
}
/**
* Closes the GATT device and cleans up.
*
* @param gatt the GATT device to be closed
*/
protected void close(final BluetoothGatt gatt) {
logi("Cleaning up...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.close()");
gatt.close();
mConnectionState = STATE_CLOSED;
}
/**
* Clears the device cache. After uploading new firmware the DFU target will have other services than before.
*
* @param gatt the GATT device to be refreshed
* @param force <code>true</code> to force the refresh
*/
protected void refreshDeviceCache(final BluetoothGatt gatt, final boolean force) {
/*
* If the device is bonded this is up to the Service Changed characteristic to notify Android that the services has changed.
* There is no need for this trick in that case.
* If not bonded, the Android should not keep the services cached when the Service Changed characteristic is present in the target device database.
* However, due to the Android bug (still exists in Android 5.0.1), it is keeping them anyway and the only way to clear services is by using this hidden refresh method.
*/
if (force || gatt.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.refresh() (hidden)");
/*
* There is a refresh() method in BluetoothGatt class but for now it's hidden. We will call it using reflections.
*/
try {
final Method refresh = gatt.getClass().getMethod("refresh");
if (refresh != null) {
final boolean success = (Boolean) refresh.invoke(gatt);
logi("Refreshing result: " + success);
}
} catch (Exception e) {
loge("An exception occurred while refreshing device", e);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Refreshing failed");
}
}
}
/**
* Enables or disables the notifications for given characteristic. This method is SYNCHRONOUS and wait until the
* {@link android.bluetooth.BluetoothGattCallback#onDescriptorWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattDescriptor, int)} will be called or the connection state will change from {@link #STATE_CONNECTED_AND_READY}. If
* connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device
* @param characteristic the characteristic to enable or disable notifications for
* @param type {@link #NOTIFICATIONS} or {@link #INDICATIONS}
* @throws DfuException
* @throws UploadAbortedException
*/
protected void enableCCCD(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int type) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
final String debugString = type == NOTIFICATIONS ? "notifications" : "indications";
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to set " + debugString + " state", mConnectionState);
mReceivedData = null;
mError = 0;
final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG);
boolean cccdEnabled = descriptor.getValue() != null && descriptor.getValue().length == 2 && descriptor.getValue()[0] > 0 && descriptor.getValue()[1] == 0;
if (cccdEnabled)
return;
logi("Enabling " + debugString + "...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Enabling " + debugString + " for " + characteristic.getUuid());
// enable notifications locally
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)");
gatt.setCharacteristicNotification(characteristic, true);
// enable notifications on the device
descriptor.setValue(type == NOTIFICATIONS ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeDescriptor(" + descriptor.getUuid() + (type == NOTIFICATIONS ? ", value=0x01-00)" : ", value=0x02-00)"));
gatt.writeDescriptor(descriptor);
// We have to wait until device receives a response or an error occur
try {
synchronized (mLock) {
while ((!cccdEnabled && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused) {
mLock.wait();
// Check the value of the CCCD
cccdEnabled = descriptor.getValue() != null && descriptor.getValue().length == 2 && descriptor.getValue()[0] > 0 && descriptor.getValue()[1] == 0;
}
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to set " + debugString + " state", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to set " + debugString + " state", mConnectionState);
}
/**
* Reads the value of the Service Changed Client Characteristic Configuration descriptor (CCCD).
*
* @param gatt the GATT device
* @return <code>true</code> if Service Changed CCCD is enabled and set to INDICATE
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
private boolean isServiceChangedCCCDEnabled(final BluetoothGatt gatt) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to read Service Changed CCCD", mConnectionState);
// If the Service Changed characteristic or the CCCD is not available we return false.
final BluetoothGattService genericAttributeService = gatt.getService(GENERIC_ATTRIBUTE_SERVICE_UUID);
if (genericAttributeService == null)
return false;
final BluetoothGattCharacteristic serviceChangedCharacteristic = genericAttributeService.getCharacteristic(SERVICE_CHANGED_UUID);
if (serviceChangedCharacteristic == null)
return false;
final BluetoothGattDescriptor descriptor = serviceChangedCharacteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG);
if (descriptor == null)
return false;
mRequestCompleted = false;
mError = 0;
logi("Reading Service Changed CCCD value...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Reading Service Changed CCCD value...");
gatt.readDescriptor(descriptor);
// We have to wait until device receives a response or an error occur
try {
synchronized (mLock) {
while ((!mRequestCompleted && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to read Service Changed CCCD", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to read Service Changed CCCD", mConnectionState);
// Return true if the CCCD value is
return descriptor.getValue() != null && descriptor.getValue().length == 2
&& descriptor.getValue()[0] == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE[0]
&& descriptor.getValue()[1] == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE[1];
}
/**
* 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 connection state will change from {@link #STATE_CONNECTED_AND_READY}.
* If connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device
* @param characteristic the characteristic to write to. Should be the DFU CONTROL POINT
* @param value the value to write to the characteristic
* @param reset whether the command trigger restarting the device
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
protected void writeOpCode(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] value, final boolean reset) throws DeviceDisconnectedException, DfuException,
UploadAbortedException {
mReceivedData = null;
mError = 0;
mRequestCompleted = false;
/*
* Sending a command that will make the DFU target to reboot may cause an error 133 (0x85 - Gatt Error). If so, with this flag set, the error will not be shown to the user
* as the peripheral is disconnected anyway. See: mGattCallback#onCharacteristicWrite(...) method
*/
mResetRequestSent = reset;
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
characteristic.setValue(value);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Writing to characteristic " + characteristic.getUuid());
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
gatt.writeCharacteristic(characteristic);
// We have to wait for confirmation
try {
synchronized (mLock) {
while ((!mRequestCompleted && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (!mResetRequestSent && mError != 0)
throw new DfuException("Unable to write Op Code " + value[0], mError);
if (!mResetRequestSent && mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to write Op Code " + value[0], mConnectionState);
}
@SuppressLint("NewApi")
protected boolean createBond(final BluetoothDevice device) {
if (device.getBondState() == BluetoothDevice.BOND_BONDED)
return true;
boolean result;
mRequestCompleted = false;
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Starting pairing...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().createBond()");
result = device.createBond();
} else {
result = createBondApi18(device);
}
// We have to wait until device is bounded
try {
synchronized (mLock) {
while (!mRequestCompleted && !mAborted)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
return result;
}
protected boolean createBondApi18(final BluetoothDevice device) {
/*
* There is a createBond() method in BluetoothDevice class but for now it's hidden. We will call it using reflections. It has been revealed in KitKat (Api19)
*/
try {
final Method createBond = device.getClass().getMethod("createBond");
if (createBond != null) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().createBond() (hidden)");
return (Boolean) createBond.invoke(device);
}
} catch (final Exception e) {
Log.w(TAG, "An exception occurred while creating bond", e);
}
return false;
}
/**
* Removes the bond information for the given device.
*
* @param device the device to unbound
* @return <code>true</code> if operation succeeded, <code>false</code> otherwise
*/
protected boolean removeBond(final BluetoothDevice device) {
if (device.getBondState() == BluetoothDevice.BOND_NONE)
return true;
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Removing bond information...");
boolean result = false;
/*
* There is a removeBond() method in BluetoothDevice class but for now it's hidden. We will call it using reflections.
*/
try {
final Method removeBond = device.getClass().getMethod("removeBond");
if (removeBond != null) {
mRequestCompleted = false;
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().removeBond() (hidden)");
result = (Boolean) removeBond.invoke(device);
// We have to wait until device is unbounded
try {
synchronized (mLock) {
while (!mRequestCompleted && !mAborted)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
}
result = true;
} catch (final Exception e) {
Log.w(TAG, "An exception occurred while removing bond information", e);
}
return result;
}
/**
* Waits until the notification will arrive. Returns the data returned by the notification. This method will block the thread if response is not ready or connection state will change from
* {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown.
*
* @return the value returned by the Control Point notification
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
protected byte[] readNotificationResponse() throws DeviceDisconnectedException, DfuException, UploadAbortedException {
// do not clear the mReceiveData here. The response might already be obtained. Clear it in write request instead.
mError = 0;
try {
synchronized (mLock) {
while ((mReceivedData == null && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Op Code", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to write Op Code", mConnectionState);
return mReceivedData;
}
protected String parse(final byte[] data) {
if (data == null)
return "";
final int length = data.length;
if (length == 0)
return "";
final char[] out = new char[length * 3 - 1];
for (int j = 0; j < length; j++) {
int v = data[j] & 0xFF;
out[j * 3] = HEX_ARRAY[v >>> 4];
out[j * 3 + 1] = HEX_ARRAY[v & 0x0F];
if (j != length - 1)
out[j * 3 + 2] = '-';
}
return new String(out);
}
protected void loge(final String message) {
Log.e(TAG, message);
}
protected void loge(final String message, final Throwable e) {
Log.e(TAG, message, e);
}
protected void logw(final String message) {
// if (BuildConfig.DEBUG)
Log.w(TAG, message);
}
protected void logi(final String message) {
// if (BuildConfig.DEBUG)
Log.i(TAG, message);
}
protected void logd(final String message) {
// if (BuildConfig.DEBUG)
Log.d(TAG, message);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
/*************************************************************************************************************************************************
* 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.dfu;
/* package */ class DfuProgressInfo {
private int progress;
private int bytesSent;
private int bytesReceived;
private int imageSizeInBytes;
private int maxObjectSizeInBytes;
private int currentPart;
private int totalParts;
public DfuProgressInfo(final int imageSizeInBytes) {
this.imageSizeInBytes = imageSizeInBytes;
this.maxObjectSizeInBytes = Integer.MAX_VALUE; // by default the whole firmware will be sent as a single object
}
public DfuProgressInfo setPart(final int currentPart, final int totalParts) {
this.currentPart = currentPart;
this.totalParts = totalParts;
return this;
}
public DfuProgressInfo setProgress(final int progress) {
this.progress = progress;
return this;
}
public DfuProgressInfo setBytesSent(final int bytesSent) {
this.bytesSent = bytesSent;
this.progress = (int) (100.0f * bytesSent / imageSizeInBytes);
return this;
}
public DfuProgressInfo addBytesSent(final int increment) {
return setBytesSent(bytesSent + increment);
}
public DfuProgressInfo setBytesReceived(final int bytesReceived) {
this.bytesReceived = bytesReceived;
return this;
}
public DfuProgressInfo addBytesReceived(final int increment) {
return setBytesReceived(bytesReceived + increment);
}
public void setMaxObjectSizeInBytes(final int bytes) {
this.maxObjectSizeInBytes = bytes;
}
public boolean isComplete() {
return bytesSent == imageSizeInBytes;
}
public boolean isObjectComplete() {
return (bytesSent % maxObjectSizeInBytes) == 0;
}
public int getAvailableObjectSizeIsBytes() {
return maxObjectSizeInBytes - (bytesSent % maxObjectSizeInBytes);
}
public int getProgress() {
return progress;
}
public int getBytesSent() {
return bytesSent;
}
public int getBytesReceived() {
return bytesReceived;
}
public int getImageSizeInBytes() {
return imageSizeInBytes;
}
public int getCurrentPart() {
return currentPart;
}
public int getTotalParts() {
return totalParts;
}
}

View File

@ -0,0 +1,323 @@
/*************************************************************************************************************************************************
* 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.dfu;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import java.io.InputStream;
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.UnknownResponseException;
import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
/* package */ class LegacyDfuImpl extends BaseCustomDfuImpl {
// DFU status values
public static final int DFU_STATUS_SUCCESS = 1;
public static final int DFU_STATUS_INVALID_STATE = 2;
public static final int DFU_STATUS_NOT_SUPPORTED = 3;
public static final int DFU_STATUS_DATA_SIZE_EXCEEDS_LIMIT = 4;
public static final int DFU_STATUS_CRC_ERROR = 5;
public static final int DFU_STATUS_OPERATION_FAILED = 6;
// 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
private static final int OP_CODE_RECEIVE_FIRMWARE_IMAGE_KEY = 0x03; // 3
private static final int OP_CODE_VALIDATE_KEY = 0x04; // 4
private static final int OP_CODE_ACTIVATE_AND_RESET_KEY = 0x05; // 5
private static final int OP_CODE_RESET_KEY = 0x06; // 6
//private static final int OP_CODE_PACKET_REPORT_RECEIVED_IMAGE_SIZE_KEY = 0x07; // 7
private static final int OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY = 0x08; // 8
private static final int OP_CODE_RESPONSE_CODE_KEY = 0x10; // 16
private static final int OP_CODE_PACKET_RECEIPT_NOTIF_KEY = 0x11; // 11
private static final byte[] OP_CODE_START_DFU = new byte[]{OP_CODE_START_DFU_KEY, 0x00};
private static final byte[] OP_CODE_INIT_DFU_PARAMS_START = new byte[]{OP_CODE_INIT_DFU_PARAMS_KEY, 0x00};
private static final byte[] OP_CODE_INIT_DFU_PARAMS_COMPLETE = new byte[]{OP_CODE_INIT_DFU_PARAMS_KEY, 0x01};
private static final byte[] OP_CODE_RECEIVE_FIRMWARE_IMAGE = new byte[]{OP_CODE_RECEIVE_FIRMWARE_IMAGE_KEY};
private static final byte[] OP_CODE_VALIDATE = new byte[]{OP_CODE_VALIDATE_KEY};
private static final byte[] OP_CODE_ACTIVATE_AND_RESET = new byte[]{OP_CODE_ACTIVATE_AND_RESET_KEY};
private static final byte[] OP_CODE_RESET = new byte[]{OP_CODE_RESET_KEY};
//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
private static final UUID DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L);
private static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L);
private static final UUID DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L);
private static final UUID DFU_VERSION = new UUID(0x000015341212EFDEL, 0x1523785FEABCD123L);
private int mFileType;
/**
* Flag indicating whether the image size has been already transferred or not
*/
private boolean mImageSizeSent;
protected class LegacyBluetoothCallback extends BaseCustomBluetoothCallback {
@Override
protected void onPacketCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
if (mImageSizeSent) {
// We've got confirmation that the image size was sent
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic));
mImageSizeSent = false;
}
}
@Override
public void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
final int responseType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
switch (responseType) {
case OP_CODE_PACKET_RECEIPT_NOTIF_KEY:
mProgressInfo.setBytesReceived(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 1));
mService.updateProgressNotification(mProgressInfo);
handlePacketReceiptNotification(gatt, characteristic);
break;
case OP_CODE_RESPONSE_CODE_KEY:
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;
}
notifyLock();
}
}
LegacyDfuImpl(final DfuBaseService service, final InputStream firmwareStream, final InputStream initPacketStream, final int packetsBeforeNotification) {
super(service, firmwareStream, initPacketStream, packetsBeforeNotification);
}
@Override
protected BluetoothGattCallback getGattCallback() {
return new LegacyBluetoothCallback();
}
@Override
protected UUID getControlPointCharacteristicUUID() {
return DFU_CONTROL_POINT_UUID;
}
@Override
protected UUID getPacketCharacteristicUUID() {
return DFU_PACKET_UUID;
}
@Override
protected UUID getDfuServiceUUID() {
return DFU_SERVICE_UUID;
}
/**
* 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);
}
/**
* 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] < 1 || response[2] > 6)
throw new UnknownResponseException("Invalid response received", response, request);
return response[2];
}
/**
* Reads the DFU Version characteristic if such exists. Otherwise it returns 0.
*
* @param gatt the GATT device
* @param characteristic the characteristic to read
* @return a version number or 0 if not present on the bootloader
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
private int readVersion(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to read version number", mConnectionState);
// If the DFU Version characteristic is not available we return 0.
if (characteristic == null)
return 0;
mReceivedData = null;
mError = 0;
logi("Reading DFU version number...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Reading DFU version number...");
characteristic.setValue((byte[]) null);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.readCharacteristic(" + characteristic.getUuid() + ")");
gatt.readCharacteristic(characteristic);
// We have to wait until device receives a response or an error occur
try {
synchronized (mLock) {
while (((!mRequestCompleted || characteristic.getValue() == null ) && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused) {
mRequestCompleted = false;
mLock.wait();
}
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to read version number", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to read version number", mConnectionState);
// The version is a 16-bit unsigned int
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0);
}
/**
* 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 connection state will change from {@link #STATE_CONNECTED_AND_READY}.
* If connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device
* @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 BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] value) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
final boolean reset = value[0] == OP_CODE_RESET_KEY || value[0] == OP_CODE_ACTIVATE_AND_RESET_KEY;
writeOpCode(gatt, characteristic, value, reset);
}
/**
* Writes the image size 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 connection state will change from {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown.
*
* @param gatt the GATT device
* @param characteristic the characteristic to write to. Should be the DFU PACKET
* @param imageSize the image size in bytes
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
private void writeImageSize(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int imageSize) throws DeviceDisconnectedException, DfuException,
UploadAbortedException {
mReceivedData = null;
mError = 0;
mImageSizeSent = true;
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
characteristic.setValue(new byte[4]);
characteristic.setValue(imageSize, BluetoothGattCharacteristic.FORMAT_UINT32, 0);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Writing to characteristic " + characteristic.getUuid());
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
gatt.writeCharacteristic(characteristic);
// We have to wait for confirmation
try {
synchronized (mLock) {
while ((mImageSizeSent && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Image Size", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to write Image Size", mConnectionState);
}
/**
* <p>
* Writes the Soft Device, Bootloader and Application image sizes to the characteristic. Soft Device and Bootloader update is supported since Soft Device s110 v7.0.0.
* Sizes of SD, BL and App are uploaded as 3x UINT32 even though some of them may be 0s. F.e. if only App is being updated the data will be <0x00000000, 0x00000000, [App size]>
* </p>
* <p>
* 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 connection state will
* change from {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown.
* </p>
*
* @param gatt the GATT device
* @param characteristic the characteristic to write to. Should be the DFU PACKET
* @param softDeviceImageSize the Soft Device image size in bytes
* @param bootloaderImageSize the Bootloader image size in bytes
* @param appImageSize the Application image size in bytes
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
private void writeImageSize(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int softDeviceImageSize, final int bootloaderImageSize, final int appImageSize)
throws DeviceDisconnectedException, DfuException, UploadAbortedException {
mReceivedData = null;
mError = 0;
mImageSizeSent = true;
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
characteristic.setValue(new byte[12]);
characteristic.setValue(softDeviceImageSize, BluetoothGattCharacteristic.FORMAT_UINT32, 0);
characteristic.setValue(bootloaderImageSize, BluetoothGattCharacteristic.FORMAT_UINT32, 4);
characteristic.setValue(appImageSize, BluetoothGattCharacteristic.FORMAT_UINT32, 8);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Writing to characteristic " + characteristic.getUuid());
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
gatt.writeCharacteristic(characteristic);
// We have to wait for confirmation
try {
synchronized (mLock) {
while ((mImageSizeSent && mConnectionState == STATE_CONNECTED_AND_READY && mError == 0 && !mAborted) || mPaused)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Image Sizes", mError);
if (mConnectionState != STATE_CONNECTED_AND_READY)
throw new DeviceDisconnectedException("Unable to write Image Sizes", mConnectionState);
}
}

View File

@ -0,0 +1,39 @@
/*************************************************************************************************************************************************
* 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.dfu;
import android.bluetooth.BluetoothGattCallback;
import java.io.InputStream;
/* package */ class SecureDfuImpl extends BaseCustomDfuImpl {
SecureDfuImpl(final DfuBaseService service, final InputStream firmwareStream, final InputStream initPacketStream, final int packetsBeforeNotification) {
super(service, firmwareStream, initPacketStream, packetsBeforeNotification);
}
@Override
protected BluetoothGattCallback getGattCallback() {
return null;
}
}

View File

@ -170,7 +170,7 @@ public class GattError {
case DfuBaseService.ERROR_INIT_PACKET_REQUIRED: case DfuBaseService.ERROR_INIT_PACKET_REQUIRED:
return "INIT PACKET REQUIRED"; return "INIT PACKET REQUIRED";
default: default:
if ((DfuBaseService.ERROR_REMOTE_MASK & error) > 0) { /*if ((DfuBaseService.ERROR_REMOTE_MASK & error) > 0) {
switch (error & (~DfuBaseService.ERROR_REMOTE_MASK)) { switch (error & (~DfuBaseService.ERROR_REMOTE_MASK)) {
case DfuBaseService.DFU_STATUS_INVALID_STATE: case DfuBaseService.DFU_STATUS_INVALID_STATE:
return "REMOTE DFU INVALID STATE"; return "REMOTE DFU INVALID STATE";
@ -183,7 +183,7 @@ public class GattError {
case DfuBaseService.DFU_STATUS_OPERATION_FAILED: case DfuBaseService.DFU_STATUS_OPERATION_FAILED:
return "REMOTE DFU OPERATION FAILED"; return "REMOTE DFU OPERATION FAILED";
} }
} }*/ // TODO
return "UNKNOWN (" + error + ")"; return "UNKNOWN (" + error + ")";
} }
} }

View File

@ -20,8 +20,7 @@
<string name="dfu_status_connecting_msg">Connecting to %s&#8230;</string> <string name="dfu_status_connecting_msg">Connecting to %s&#8230;</string>
<string name="dfu_status_starting_msg">Initializing DFU process&#8230;</string> <string name="dfu_status_starting_msg">Initializing DFU process&#8230;</string>
<string name="dfu_status_switching_to_dfu_msg">Waiting for bootloader&#8230;</string> <string name="dfu_status_switching_to_dfu_msg">Waiting for bootloader&#8230;</string>
<string name="dfu_status_uploading_components_msg">Transmitting components to %s&#8230;</string> <string name="dfu_status_uploading_msg">Transmitting firmware to %s&#8230;</string>
<string name="dfu_status_uploading_msg">Transmitting application to %s&#8230;</string>
<string name="dfu_status_validating_msg">Validating&#8230;</string> <string name="dfu_status_validating_msg">Validating&#8230;</string>
<string name="dfu_status_completed_msg">Application has been send successfully.</string> <string name="dfu_status_completed_msg">Application has been send successfully.</string>
<string name="dfu_status_aborted_msg">Application upload has been canceled.</string> <string name="dfu_status_aborted_msg">Application upload has been canceled.</string>