From f16a96e9b226c6b563acfe3c11c3eddbcaea30ad Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Thu, 23 Jul 2015 17:14:51 +0200 Subject: [PATCH] Initial support for upgrading firmware of the MiBand. This release seems to be working quite well with respect to the firmware upgrading itself. The user facing part needs more work. In order to update the firmware one has to: - open a file ending with .fw - switch from the firmware upgrade activity to the main one - connect to the miband - return to the firmware upgrade activity - press the "install" button (that became active when the device connection was established) Caveats: There are almost no check wrt. the integrity of the firmware files. --- app/src/main/AndroidManifest.xml | 30 +++++ .../gadgetbridge/miband/DeviceInfo.java | 11 +- .../gadgetbridge/miband/FwUpgrade.java | 106 ++++++++++++++++ .../gadgetbridge/miband/MiBandFWHelper.java | 97 +++++++++++++++ .../gadgetbridge/miband/MiBandService.java | 8 +- .../gadgetbridge/miband/MiBandSupport.java | 116 +++++++++++++++++- .../main/res/layout/activity_fw_upgrade.xml | 21 ++++ app/src/main/res/values/strings.xml | 5 + 8 files changed, 387 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/FwUpgrade.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandFWHelper.java create mode 100644 app/src/main/res/layout/activity_fw_upgrade.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b17bd9d51..d83ef107c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -203,6 +203,36 @@ android:name="android.support.PARENT_ACTIVITY" android:value="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms" /> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/DeviceInfo.java index da8315497..4f68b5979 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/DeviceInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/DeviceInfo.java @@ -10,11 +10,20 @@ public class DeviceInfo extends AbstractInfo { super(data); } - public String getFirmwareVersion() { + public String getHumanFirmwareVersion() { if (mData.length == 16) { int last = 15; return String.format(Locale.US, "%d.%d.%d.%d", mData[last], mData[last - 1], mData[last - 2], mData[last - 3]); } return GBApplication.getContext().getString(R.string._unknown_); } + + public int getFirmwareVersion() { + if (mData.length == 16) { + int last = 15; + return (mData[last] << 24) | (mData[last - 1] << 16) | (mData[last - 2] << 8) | mData[last - 3]; + } + return -1; + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/FwUpgrade.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/FwUpgrade.java new file mode 100644 index 000000000..1f5c4e4d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/FwUpgrade.java @@ -0,0 +1,106 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v4.content.LocalBroadcastManager; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import nodomain.freeyourgadget.gadgetbridge.BluetoothCommunicationService; +import nodomain.freeyourgadget.gadgetbridge.ControlCenter; +import nodomain.freeyourgadget.gadgetbridge.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.R; + + +/* +TODO: This could be moved to activities package and merged with pebble/PebbleAppInstallerActivity.java + */ + +public class FwUpgrade extends Activity { + + + TextView fwUpgradeTextView; + Button installButton; + + private MiBandFWHelper mFwReader = null; + private GBDevice dev; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(ControlCenter.ACTION_QUIT)) { + finish(); + } else if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) { + dev = intent.getParcelableExtra("device"); + if(dev.getType() == DeviceType.MIBAND) { + if (dev.isInitialized() && mFwReader != null) { + installButton.setEnabled(true); + } + } + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_fw_upgrade); + + fwUpgradeTextView = (TextView) findViewById(R.id.fwUpgradeTextView); + installButton = (Button) findViewById(R.id.installButton); + + IntentFilter filter = new IntentFilter(); + filter.addAction(ControlCenter.ACTION_QUIT); + filter.addAction(GBDevice.ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter); + + final Uri uri = getIntent().getData(); + mFwReader = new MiBandFWHelper(uri, getApplicationContext()); + + fwUpgradeTextView.setText(getString(R.string.fw_upgrade_notice, mFwReader.getHumanFirmwareVersion())); + + if (mFwReader.isFirmwareWhitelisted()) { + fwUpgradeTextView.append(" " + getString(R.string.miband_firmware_known)); + }else { + fwUpgradeTextView.append(" " + getString(R.string.miband_firmware_unknown_warning) + " " + + getString(R.string.miband_firmware_suggest_whitelist, mFwReader.getFirmwareVersion())); + } + + installButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent startIntent = new Intent(FwUpgrade.this, BluetoothCommunicationService.class); + startIntent.setAction(BluetoothCommunicationService.ACTION_INSTALL); + startIntent.putExtra("uri", uri.toString()); + startService(startIntent); + } + }); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); + super.onDestroy(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandFWHelper.java new file mode 100644 index 000000000..2759f0970 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandFWHelper.java @@ -0,0 +1,97 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.util.Locale; + +public class MiBandFWHelper { + private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class); + + private final Uri uri; + private final ContentResolver cr; + private byte[] fw; + + private final int firmwareVersionBuild = 1056; + private final int firmwareVersionRevision = 1057; + private final int firmwareVersionMinor = 1058; + private final int firmwareVersionMajor = 1059; + + private final int[] whitelistedFirmwareVersion = { + 16779534, // 1.0.9.14 tested by developer + 16779547 //1.0.9.27 testd by developer + }; + + public MiBandFWHelper(Uri uri, Context context) { + this.uri = uri; + cr = context.getContentResolver(); + + InputStream fin; + + try { + fin = new BufferedInputStream(cr.openInputStream(uri)); + this.fw = new byte[fin.available()]; + fin.read(fw); + fin.close(); + + } catch (Exception e) { + e.printStackTrace(); + this.fw = null; + } + + if (fw[firmwareVersionMajor] != 1 ) { + LOG.error("Firmware major version should be 1, probably this isn't a MiBand firmware."); + this.fw = null; + } + + } + + public int getFirmwareVersion() { + if(fw == null) { + return -1; + } + return (fw[firmwareVersionMajor] << 24) | (fw[firmwareVersionMinor] << 16) | (fw[firmwareVersionRevision] << 8) | fw[firmwareVersionBuild]; + } + + public String getHumanFirmwareVersion() { + if(fw == null) { + return "UNK"; + } + return String.format(Locale.US, "%d.%d.%d.%d", fw[firmwareVersionMajor], fw[firmwareVersionMinor], fw[firmwareVersionRevision], fw[firmwareVersionBuild]); + } + + public byte[] getFw() { + return fw; + } + + public boolean isFirmwareWhitelisted() { + for (int wlf : whitelistedFirmwareVersion) { + if (wlf == getFirmwareVersion()) { + return true; + } + } + return false; + } + + //thanks http://stackoverflow.com/questions/13209364/convert-c-crc16-to-java-crc16 + public int getCRC16(byte[] seq) { + int crc = 0xFFFF; + + for (int j = 0; j < seq.length ; j++) { + crc = ((crc >>> 8) | (crc << 8) )& 0xffff; + crc ^= (seq[j] & 0xff);//byte to int, trunc sign + crc ^= ((crc & 0xff) >> 4); + crc ^= (crc << 12) & 0xffff; + crc ^= ((crc & 0xFF) << 5) & 0xffff; + } + crc &= 0xffff; + return crc; + } + + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java index e9500f365..be9f288f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java @@ -137,6 +137,11 @@ public class MiBandService { public static final byte COMMAND_SET_TIMER = 0x4; + public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7; + + public static final byte COMMAND_SYNC = 0xb; + + /* @@ -145,7 +150,6 @@ public class MiBandService { public static final byte COMMAND_GET_SENSOR_DATA = 0x12t - public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7t public static final int COMMAND_SET_COLOR_THEME = et; @@ -159,8 +163,6 @@ public class MiBandService { public static final COMMAND_STOP_SYNC_DATA = 0x11t - public static final COMMAND_SYNC = 0xbt - public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t; public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java index 33da795d0..4eaab5e98 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java @@ -80,6 +80,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { private ActivityStruct activityStruct; + private DeviceInfo mDeviceInfo; + public MiBandSupport() { addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); } @@ -531,7 +533,19 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { @Override public void onInstallApp(Uri uri) { - // not supported + MiBandFWHelper mFwHelper = new MiBandFWHelper(uri, getContext()); + String mMac = getDevice().getAddress(); + String[] mMacOctets = mMac.split(":"); + + int newFwVersion = mFwHelper.getFirmwareVersion(); + int oldFwVersion = mDeviceInfo.getFirmwareVersion(); + int checksum = (Integer.decode("0x" + mMacOctets[4]) << 8 | Integer.decode("0x" + mMacOctets[5])) ^ mFwHelper.getCRC16(mFwHelper.getFw()); + + sendFirmwareInfo(oldFwVersion,newFwVersion, mFwHelper.getFw().length, checksum); + sendFirmwareData(mFwHelper.getFw()); + onReboot(); + + return; } @Override @@ -602,8 +616,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { private void handleDeviceInfo(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { - DeviceInfo info = new DeviceInfo(value); - getDevice().setFirmwareVersion(info.getFirmwareVersion()); + mDeviceInfo = new DeviceInfo(value); + getDevice().setFirmwareVersion(mDeviceInfo.getHumanFirmwareVersion()); getDevice().sendDeviceUpdateIntent(getContext()); } } @@ -830,6 +844,102 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { LOG.info("MI Band pairing result: " + value); } + private void sendFirmwareInfo(int currentFwVersion, int newFwVersion, int newFwSize, int checksum) { + byte[] fwInfo = new byte[]{ + MiBandService.COMMAND_SEND_FIRMWARE_INFO, + (byte) currentFwVersion, + (byte) (currentFwVersion >> 8), + (byte) (currentFwVersion >> 16), + (byte) (currentFwVersion >> 24), + (byte) newFwVersion, + (byte) (newFwVersion >> 8), + (byte) (newFwVersion >> 16), + (byte) (newFwVersion >> 24), + (byte) newFwSize, + (byte) (newFwSize >> 8), + (byte) checksum, + (byte) (checksum >> 8) + }; + try { + TransactionBuilder builder = performInitialized("send firmware info"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fwInfo); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send fwInfo to MI", ex); + } + } + + private void sendFirmwareData(byte fwbytes[]) { + int len = fwbytes.length; + final int packetLength = 20; + int packets = len / packetLength; + byte fwChunk[] = new byte[packetLength]; + + int firmwareProgress = 0; + + for (int i = 0; i < packets; i++) { + fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); + + if (!sendFirmwareChunk(fwChunk)) { + LOG.error("Firmware chunk write failed"); + return; + } + + firmwareProgress += packetLength; + + if ((i > 0) && (i % 50 == 0)) { + if(!sendFirmwareSync()) { + LOG.error("Firmware sync failed"); + return; + } + } + LOG.info("Firmware update progress:" + firmwareProgress + " total lenL:" + len + " progress:" + firmwareProgress / len); + } + + if (!(len % packetLength == 0)) { + byte lastChunk[] = new byte[len % packetLength]; + lastChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len); + if (!sendFirmwareChunk(lastChunk)) { + LOG.error("Firmware chunk write failed"); + return; + } + firmwareProgress += len % packetLength; + } + + LOG.info("Firmware update progress:" + firmwareProgress +" total lenL:"+ len + " progress:" + firmwareProgress/len); + + if(!sendFirmwareSync()) { + LOG.error("Firmware sync failed"); + return; + } + + } + + private boolean sendFirmwareChunk(byte fwChunk[]) { + try { + TransactionBuilder builder = performInitialized("send firmware packet"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_FIRMWARE_DATA), fwChunk); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send fw packet to MI", ex); + return false; + } + return true; + } + + private boolean sendFirmwareSync() { + try { + TransactionBuilder builder = performInitialized("send firmware sync"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), new byte[] {MiBandService.COMMAND_SYNC}); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send firmware sync to MI", ex); + return false; + } + return true; + } + + @Override protected TransactionBuilder createTransactionBuilder(String taskName) { return new MiBandTransactionBuilder(taskName); diff --git a/app/src/main/res/layout/activity_fw_upgrade.xml b/app/src/main/res/layout/activity_fw_upgrade.xml new file mode 100644 index 000000000..c8c388660 --- /dev/null +++ b/app/src/main/res/layout/activity_fw_upgrade.xml @@ -0,0 +1,21 @@ + + + +