Mi2: Initial work on firmware update #427

here
cpfeiffer 2016-12-11 02:10:07 +01:00
parent 4a19046301
commit 6dfc895303
16 changed files with 746 additions and 149 deletions

View File

@ -0,0 +1,100 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
/**
* Also see Mi1SFirmwareInfo.
*/
public abstract class AbstractMiBandFWHelper {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWHelper.class);
@NonNull
private final byte[] fw;
public AbstractMiBandFWHelper(Uri uri, Context context) throws IOException {
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
if (uri.getPath().matches(pebblePattern)) {
throw new IOException("Firmware has a filename that looks like a Pebble app/firmware.");
}
try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) {
this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB
determineFirmwareInfo(fw);
} catch (IOException ex) {
throw ex; // pass through
} catch (IllegalArgumentException ex) {
throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex);
} catch (Exception e) {
throw new IOException("Error reading firmware file: " + uri.toString(), e);
}
}
public abstract int getFirmwareVersion();
public abstract int getFirmware2Version();
public static String formatFirmwareVersion(int version) {
if (version == -1)
return GBApplication.getContext().getString(R.string._unknown_);
return String.format("%d.%d.%d.%d",
version >> 24 & 255,
version >> 16 & 255,
version >> 8 & 255,
version & 255);
}
public String getHumanFirmwareVersion() {
return format(getFirmwareVersion());
}
public abstract String getHumanFirmwareVersion2();
public String format(int version) {
return formatFirmwareVersion(version);
}
@NonNull
public byte[] getFw() {
return fw;
}
public boolean isFirmwareWhitelisted() {
for (int wlf : getWhitelistedFirmwareVersions()) {
if (wlf == getFirmwareVersion()) {
return true;
}
}
return false;
}
protected abstract int[] getWhitelistedFirmwareVersions();
public abstract boolean isFirmwareGenerallyCompatibleWith(GBDevice device);
public abstract boolean isSingleFirmware();
/**
* @param wholeFirmwareBytes
* @return
* @throws IllegalArgumentException when the data is not recognized as firmware data
*/
@NonNull
protected abstract void determineFirmwareInfo(byte[] wholeFirmwareBytes);
public abstract void checkValid() throws IllegalArgumentException;
}

View File

@ -0,0 +1,103 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWInstallHandler.class);
private final Context mContext;
private AbstractMiBandFWHelper helper;
private String errorMessage;
public AbstractMiBandFWInstallHandler(Uri uri, Context context) {
mContext = context;
try {
helper = createHelper(uri, context);
} catch (IOException e) {
errorMessage = e.getMessage();
LOG.warn(errorMessage, e);
}
}
protected abstract AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException;
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
if (!isSupportedDeviceType(device) || !device.isInitialized()) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
installActivity.setInstallEnabled(false);
return;
}
try {
helper.checkValid();
} catch (IllegalArgumentException ex) {
installActivity.setInfoText(ex.getLocalizedMessage());
installActivity.setInstallEnabled(false);
return;
}
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
fwItem.setIcon(R.drawable.ic_device_miband);
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
installActivity.setInstallEnabled(false);
return;
}
StringBuilder builder = new StringBuilder();
if (helper.isSingleFirmware()) {
builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion()));
} else {
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
}
if (helper.isFirmwareWhitelisted()) {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
// TODO: set a CHECK (OKAY) button
} else {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion()));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
// TODO: set a UNKNOWN (question mark) button
}
installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true);
}
protected abstract boolean isSupportedDeviceType(GBDevice device);
@Override
public void onStartInstall(GBDevice device) {
}
@Override
public boolean isValid() {
return helper != null;
}
}

View File

@ -19,6 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -86,11 +87,6 @@ public class MiBand2Coordinator extends MiBandCoordinator {
return new MiBand2SampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null; // not supported at the moment
}
public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException {
Prefs prefs = GBApplication.getPrefs();
String dateFormatTime = context.getString(R.string.p_dateformat_time);
@ -104,4 +100,10 @@ public class MiBand2Coordinator extends MiBandCoordinator {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
}

View File

@ -12,7 +12,7 @@ public class MiBand2Service {
public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
public static final UUID UUID_SERVICE_WEIGHT_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
public static final UUID UUID_SERVICE_FIRMWARE_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_FIRMWARE = UUID.fromString("00001531-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700");
@ -105,11 +105,11 @@ public class MiBand2Service {
public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01;
public static final byte COMMAND_ACTIVITY_DATA_XXX_DATE = 0x02; // issued on first connect, followd by COMMAND_XXXX_ACTIVITY_DATA instead of COMMAND_FETCH_ACTIVITY_DATA
public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE, followed by fw file size in bytes
public static final byte COMMAND_FIRMWARE_START_DATA = 0x03; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_UPDATE_SYNC = 0x00; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_FINISH = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_APPLY = 0x05; // or is it REBOOT? to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_APPLY_REBOOT = 0x05; // or is it REBOOT? to UUID_CHARACTERISTIC_FIRMWARE
public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
public static final byte[] RESPONSE_FIRMWARE_DATA_SUCCESS = new byte[] {RESPONSE, COMMAND_FIRMWARE_START_DATA, SUCCESS };

View File

@ -7,20 +7,15 @@ import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
/**
* Also see Mi1SFirmwareInfo.
*/
public class MiBandFWHelper {
public class MiBandFWHelper extends AbstractMiBandFWHelper {
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class);
/**
@ -29,9 +24,7 @@ public class MiBandFWHelper {
* attempting to flash it.
*/
@NonNull
private final AbstractMiFirmwareInfo firmwareInfo;
@NonNull
private final byte[] fw;
private AbstractMiFirmwareInfo firmwareInfo;
/**
* Provides a different notification API which is also used on Mi1A devices.
@ -54,77 +47,55 @@ public class MiBandFWHelper {
};
public MiBandFWHelper(Uri uri, Context context) throws IOException {
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
if (uri.getPath().matches(pebblePattern)) {
throw new IOException("Firmware has a filename that looks like a Pebble app/firmware.");
}
try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) {
this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB
this.firmwareInfo = determineFirmwareInfoFor(fw);
} catch (IOException ex) {
throw ex; // pass through
} catch (IllegalArgumentException ex) {
throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex);
} catch (Exception e) {
throw new IOException("Error reading firmware file: " + uri.toString(), e);
}
super(uri, context);
}
@Override
public int getFirmwareVersion() {
// FIXME: UnsupportedOperationException!
return firmwareInfo.getFirst().getFirmwareVersion();
}
@Override
public int getFirmware2Version() {
return firmwareInfo.getFirst().getFirmwareVersion();
}
public static String formatFirmwareVersion(int version) {
if (version == -1)
return GBApplication.getContext().getString(R.string._unknown_);
return String.format("%d.%d.%d.%d",
version >> 24 & 255,
version >> 16 & 255,
version >> 8 & 255,
version & 255);
}
public String getHumanFirmwareVersion() {
return format(getFirmwareVersion());
}
@Override
public String getHumanFirmwareVersion2() {
return format(firmwareInfo.getSecond().getFirmwareVersion());
}
public String format(int version) {
return formatFirmwareVersion(version);
}
@NonNull
public byte[] getFw() {
return fw;
}
public boolean isFirmwareWhitelisted() {
for (int wlf : whitelistedFirmwareVersion) {
if (wlf == getFirmwareVersion()) {
return true;
}
}
return false;
@Override
protected int[] getWhitelistedFirmwareVersions() {
return whitelistedFirmwareVersion;
}
@Override
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
return firmwareInfo.isGenerallyCompatibleWith(device);
}
@Override
public boolean isSingleFirmware() {
return firmwareInfo.isSingleMiBandFirmware();
}
/**
* @param wholeFirmwareBytes
* @return
* @throws IllegalArgumentException when the data is not recognized as firmware data
*/
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = AbstractMiFirmwareInfo.determineFirmwareInfoFor(wholeFirmwareBytes);
}
@Override
public void checkValid() throws IllegalArgumentException {
firmwareInfo.checkValid();
}
/**
* @param wholeFirmwareBytes
* @return

View File

@ -8,91 +8,23 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
public class MiBandFWInstallHandler implements InstallHandler {
public class MiBandFWInstallHandler extends AbstractMiBandFWInstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWInstallHandler.class);
private final Context mContext;
private MiBandFWHelper helper;
private String errorMessage;
public MiBandFWInstallHandler(Uri uri, Context context) {
mContext = context;
try {
helper = new MiBandFWHelper(uri, mContext);
} catch (IOException e) {
errorMessage = e.getMessage();
LOG.warn(errorMessage, e);
}
super(uri, context);
}
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
if (device.getType() != DeviceType.MIBAND || !device.isInitialized()) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
installActivity.setInstallEnabled(false);
return;
}
try {
helper.getFirmwareInfo().checkValid();
} catch (IllegalArgumentException ex) {
installActivity.setInfoText(ex.getLocalizedMessage());
installActivity.setInstallEnabled(false);
return;
}
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
fwItem.setIcon(R.drawable.ic_device_miband);
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
installActivity.setInstallEnabled(false);
return;
}
StringBuilder builder = new StringBuilder();
if (helper.isSingleFirmware()) {
builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion()));
} else {
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
}
if (helper.isFirmwareWhitelisted()) {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
// TODO: set a CHECK (OKAY) button
} else {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion()));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
// TODO: set a UNKNOWN (question mark) button
}
installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true);
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new MiBandFWHelper(uri, context);
}
@Override
public void onStartInstall(GBDevice device) {
}
public boolean isValid() {
return helper != null;
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.MIBAND;
}
}

View File

@ -0,0 +1,70 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband2;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo;
public class MiBand2FWHelper extends AbstractMiBandFWHelper {
private Mi2FirmwareInfo firmwareInfo;
public MiBand2FWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
@Override
public String format(int version) {
return Mi2FirmwareInfo.toVersion(version);
}
@Override
public int getFirmwareVersion() {
return firmwareInfo.getFirmwareVersion();
}
@Override
public int getFirmware2Version() {
return 0;
}
@Override
public String getHumanFirmwareVersion2() {
return "";
}
@Override
protected int[] getWhitelistedFirmwareVersions() {
return Mi2FirmwareInfo.getWhitelistedVersions();
}
@Override
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
return firmwareInfo.isGenerallyCompatibleWith(device);
}
@Override
public boolean isSingleFirmware() {
return true;
}
@NonNull
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new Mi2FirmwareInfo(wholeFirmwareBytes);
}
@Override
public void checkValid() throws IllegalArgumentException {
firmwareInfo.checkValid();
}
public Mi2FirmwareInfo getFirmwareInfo() {
return firmwareInfo;
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband2;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class MiBand2FWInstallHandler extends AbstractMiBandFWInstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(MiBand2FWInstallHandler.class);
public MiBand2FWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new MiBand2FWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.MIBAND2;
}
}

View File

@ -148,6 +148,24 @@ public class BLETypeConversions {
(byte) ((value >> 8) & 0xff),
};
}
public static byte[] fromUint24(int value) {
return new byte[] {
(byte) (value & 0xff),
(byte) ((value >> 8) & 0xff),
(byte) ((value >> 16) & 0xff),
};
}
public static byte[] fromUint32(int value) {
return new byte[] {
(byte) (value & 0xff),
(byte) ((value >> 8) & 0xff),
(byte) ((value >> 16) & 0xff),
(byte) ((value >> 24) & 0xff),
};
}
public static byte fromUint8(int value) {
return (byte) (value & 0xff);
}

View File

@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.Hear
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -130,6 +131,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE);
deviceInfoProfile = new DeviceInfoProfile<>(this);
addSupportedProfile(deviceInfoProfile);
@ -768,12 +770,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
@Override
public void onInstallApp(Uri uri) {
// TODO: onInstallApp (firmware update)
// try {
// new UpdateFirmwareOperation(uri, this).perform();
// } catch (IOException ex) {
// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
// }
try {
new UpdateFirmwareOperation(uri, this).perform();
} catch (IOException ex) {
GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
@Override

View File

@ -17,7 +17,7 @@ public abstract class AbstractMiBandOperation<T extends AbstractBTLEDeviceSuppor
@Override
protected void prePerform() throws IOException {
super.prePerform();
getDevice().setBusyTask("fetch activity data"); // mark as busy quickly to avoid interruptions from the outside
getDevice().setBusyTask("Operation starting..."); // mark as busy quickly to avoid interruptions from the outside
TransactionBuilder builder = performInitialized("disabling some notifications");
enableOtherNotifications(builder, false);
enableNeededNotifications(builder, true);

View File

@ -0,0 +1,87 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
import java.util.HashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
public class Mi2FirmwareInfo {
private static final byte[] FW_HEADER = new byte[]{
(byte) 0xa3,
(byte) 0x68,
(byte) 0x04,
(byte) 0x3b,
(byte) 0x02,
(byte) 0xdb,
(byte) 0xc8,
(byte) 0x58,
(byte) 0xd0,
(byte) 0x50,
(byte) 0xfa,
(byte) 0xe7,
(byte) 0x0c,
(byte) 0x34,
(byte) 0xf3,
(byte) 0xe7,
};
private static final int FW_HEADER_OFFSET = 0x150;
private static Map<Integer,String> crcToVersion = new HashMap<>();
static {
crcToVersion.put(41899, "1.0.0.39");
}
public static String toVersion(int crc16) {
return crcToVersion.get(crc16);
}
public static int[] getWhitelistedVersions() {
return ArrayUtils.toIntArray(crcToVersion.keySet());
}
private final int crc16;
private byte[] bytes;
private String firmwareVersion;
public Mi2FirmwareInfo(byte[] bytes) {
this.bytes = bytes;
crc16 = CheckSums.getCRC16(bytes);
firmwareVersion = crcToVersion.get(crc16);
}
public boolean isGenerallyCompatibleWith(GBDevice device) {
return isHeaderValid() && device.getType() == DeviceType.MIBAND2;
}
protected boolean isHeaderValid() {
// TODO: not sure if this is a correct check!
return ArrayUtils.equals(FW_HEADER, bytes, FW_HEADER_OFFSET, FW_HEADER_OFFSET + FW_HEADER.length);
}
public void checkValid() throws IllegalArgumentException {
}
/**
* Returns the size of the firmware in number of bytes.
* @return
*/
public int getSize() {
return bytes.length;
}
public byte[] getBytes() {
return bytes;
}
public int getCrc16() {
return crc16;
}
public int getFirmwareVersion() {
return getCrc16(); // HACK until we know how to determine the version from the fw bytes
}
}

View File

@ -0,0 +1,257 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.net.Uri;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class);
private final Uri uri;
private final BluetoothGattCharacteristic fwCControlChar;
private final BluetoothGattCharacteristic fwCDataChar;
final Prefs prefs = GBApplication.getPrefs();
private Mi2FirmwareInfo firmwareInfo;
public UpdateFirmwareOperation(Uri uri, MiBand2Support support) {
super(support);
this.uri = uri;
fwCControlChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE);
fwCDataChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE_DATA);
}
@Override
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
builder.notify(fwCControlChar, enable);
}
@Override
protected void doPerform() throws IOException {
MiBand2FWHelper mFwHelper = new MiBand2FWHelper(uri, getContext());
firmwareInfo = mFwHelper.getFirmwareInfo();
if (!firmwareInfo.isGenerallyCompatibleWith(getDevice())) {
throw new IOException("Firmware is not compatible with the given device: " + getDevice().getAddress());
}
if (!sendFwInfo()) {
displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR);
done();
}
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
}
private void done() {
LOG.info("Operation done.");
operationFinished();
unsetBusy();
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
if (fwCControlChar.getUuid().equals(characteristicUUID)) {
handleNotificationNotif(characteristic.getValue());
} else {
super.onCharacteristicChanged(gatt, characteristic);
}
return false;
}
/**
* React to messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION
* characteristic,
* These messages appear to be always 1 byte long, with values that are listed in MiBandService.
* It is not excluded that there are further values which are still unknown.
* <p/>
* Upon receiving known values that request further action by GB, the appropriate method is called.
*
* @param value
*/
private void handleNotificationNotif(byte[] value) {
if (value.length != 3) {
LOG.error("Notifications should be 3 bytes long.");
getSupport().logMessageContent(value);
return;
}
boolean success = value[2] == MiBand2Service.SUCCESS;
if (value[0] == MiBand2Service.RESPONSE && success) {
try {
switch (value[1]) {
case MiBand2Service.COMMAND_FIRMWARE_INIT: {
sendFirmwareData(getFirmwareInfo());
break;
}
case MiBand2Service.COMMAND_FIRMWARE_START_DATA: {
sendChecksum(getFirmwareInfo());
break;
}
case MiBand2Service.COMMAND_FIRMWARE_CHECKSUM: {
sendApplyReboot(getFirmwareInfo());
break;
}
case MiBand2Service.COMMAND_FIRMWARE_APPLY_REBOOT: {
GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext());
// getSupport().onReboot();
done();
break;
}
default: {
LOG.error("Unexpected response during firmware update: ");
getSupport().logMessageContent(value);
displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR);
done();
return;
}
}
} catch (Exception ex) {
displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR);
done();
}
} else {
LOG.error("Unexpected notification during firmware update: ");
getSupport().logMessageContent(value);
displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_metadata_updateproblem), Toast.LENGTH_LONG, GB.ERROR);
done();
}
}
private void displayMessage(Context context, String message, int duration, int severity) {
getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, duration, severity));
}
public boolean sendFwInfo() {
try {
TransactionBuilder builder = performInitialized("send firmware info");
// getSupport().setLowLatency(builder);
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.updating_firmware), getContext()));
int fwSize = getFirmwareInfo().getSize();
byte[] sizeBytes = BLETypeConversions.fromUint24(fwSize);
byte[] bytes = new byte[]{
MiBand2Service.COMMAND_FIRMWARE_INIT,
sizeBytes[0],
sizeBytes[1],
sizeBytes[2],
};
builder.write(fwCControlChar, bytes);
builder.queue(getQueue());
return true;
} catch (IOException e) {
LOG.error("Error sending firmware info: " + e.getLocalizedMessage(), e);
return false;
}
}
/**
* Method that uploads a firmware (fwbytes) to the Mi Band.
* The firmware has to be split into chunks of 20 bytes each, and periodically a COMMAND_SYNC command has to be issued to the Mi Band.
* <p/>
* The Mi Band will send a notification after receiving this data to confirm if the firmware looks good to it.
*
* @param info
* @return whether the transfer succeeded or not. Only a BT layer exception will cause the transmission to fail.
* @see MiBand2Support#handleNotificationNotif
*/
private boolean sendFirmwareData(Mi2FirmwareInfo info) {
byte[] fwbytes = info.getBytes();
int len = fwbytes.length;
final int packetLength = 20;
int packets = len / packetLength;
try {
// going from 0 to len
int firmwareProgress = 0;
TransactionBuilder builder = performInitialized("send firmware packet");
if (prefs.getBoolean("mi_low_latency_fw_update", true)) {
getSupport().setLowLatency(builder);
}
builder.write(fwCControlChar, new byte[] { MiBand2Service.COMMAND_FIRMWARE_START_DATA });
for (int i = 0; i < packets; i++) {
byte[] fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength);
builder.write(fwCDataChar, fwChunk);
firmwareProgress += packetLength;
int progressPercent = (int) ((((float) firmwareProgress) / len) * 100);
if ((i > 0) && (i % 100 == 0)) {
builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC});
builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext()));
}
}
if (firmwareProgress < len) {
byte[] lastChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len);
builder.write(fwCDataChar, lastChunk);
firmwareProgress = len;
}
builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC});
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to send fw to MI 2", ex);
GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_firmware_not_sent), false, 0, getContext());
return false;
}
return true;
}
private void sendChecksum(Mi2FirmwareInfo firmwareInfo) throws IOException {
TransactionBuilder builder = performInitialized("send firmware checksum");
int crc16 = firmwareInfo.getCrc16();
byte[] bytes = BLETypeConversions.fromUint16(crc16);
builder.write(fwCControlChar, new byte[] {
MiBand2Service.COMMAND_FIRMWARE_CHECKSUM,
bytes[0],
bytes[1],
});
builder.queue(getQueue());
}
private void sendApplyReboot(Mi2FirmwareInfo firmwareInfo) throws IOException {
TransactionBuilder builder = performInitialized("send firmware apply/reboot");
builder.write(fwCControlChar, new byte[] { MiBand2Service.COMMAND_FIRMWARE_APPLY_REBOOT });
builder.queue(getQueue());
}
private Mi2FirmwareInfo getFirmwareInfo() {
return firmwareInfo;
}
enum State {
INITIAL,
SEND_FW2,
SEND_FW1,
FINISHED,
UNKNOWN
}
}

View File

@ -1,5 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import java.util.Collection;
public class ArrayUtils {
/**
* Checks the two given arrays for equality, but comparing only a subset of the second
@ -25,10 +27,10 @@ public class ArrayUtils {
if (second.length < secondEndIndex) {
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
}
if (first.length < secondEndIndex) {
int len = secondEndIndex - secondStartIndex;
if (first.length != len) {
return false;
}
int len = secondEndIndex - secondStartIndex;
for (int i = 0; i < len; i++) {
if (first[i] != second[secondStartIndex + i]) {
return false;
@ -36,4 +38,22 @@ public class ArrayUtils {
}
return true;
}
/**
* Converts a collection of Integer values to an int[] array.
* @param values
* @return null if the given collection is null, otherwise an array of the same size as the collection
* @throws NullPointerException when an element of the collection is null
*/
public static int[] toIntArray(Collection<Integer> values) {
if (values == null) {
return null;
}
int i = 0;
int[] result = new int[values.size()];
for (Integer value : values) {
result[i++] = value;
}
return result;
}
}

View File

@ -4,12 +4,17 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
/**
* A simple class for trying out things, not actually testing something.
@ -32,5 +37,4 @@ public class Tryout extends TestBase {
LOG.info("Calender: " + DateTimeUtils.formatDateTime(calendar.getTime()));
Logging.logBytes(LOG, bytes);
}
}

View File

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
classpath 'com.android.tools.build:gradle:2.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files