mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
More work on firmware detection, recognition and validation #234
Should be as robust as possible now.
This commit is contained in:
parent
1933e2bf10
commit
424d9cd142
2
.gitignore
vendored
2
.gitignore
vendored
@ -29,3 +29,5 @@ proguard/
|
|||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
MPChartLib
|
MPChartLib
|
||||||
|
|
||||||
|
fw.dirs
|
||||||
|
@ -5,18 +5,37 @@ import android.support.annotation.NonNull;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some helper methods for Mi1 and Mi1A firmware.
|
* Some helper methods for Mi1 and Mi1A firmware.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class);
|
||||||
|
|
||||||
|
private static final byte[] SINGLE_FW_HEADER = new byte[] {
|
||||||
|
0,
|
||||||
|
(byte)0x98,
|
||||||
|
0,
|
||||||
|
(byte)0x20,
|
||||||
|
(byte)0x89,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
(byte)0x20
|
||||||
|
};
|
||||||
|
private static final int SINGLE_FW_HEADER_OFFSET = 0;
|
||||||
|
|
||||||
private static final int MI1_FW_BASE_OFFSET = 1056;
|
private static final int MI1_FW_BASE_OFFSET = 1056;
|
||||||
|
|
||||||
protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) {
|
protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) {
|
||||||
super(wholeFirmwareBytes);
|
super(wholeFirmwareBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleMiBandFirmware() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getFirmwareOffset() {
|
public int getFirmwareOffset() {
|
||||||
return 0;
|
return 0;
|
||||||
@ -51,11 +70,11 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isGenerallySupportedFirmware() {
|
protected boolean isGenerallySupportedFirmware() {
|
||||||
if (!isSingleMiBandFirmware()) {
|
|
||||||
LOG.warn("not a single firmware");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
if (!isHeaderValid()) {
|
||||||
|
LOG.info("unrecognized header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int majorVersion = getFirmwareVersionMajor();
|
int majorVersion = getFirmwareVersionMajor();
|
||||||
if (majorVersion == getSupportedMajorVersion()) {
|
if (majorVersion == getSupportedMajorVersion()) {
|
||||||
return true;
|
return true;
|
||||||
@ -70,5 +89,19 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isHeaderValid() {
|
||||||
|
// TODO: not sure if this is a correct check!
|
||||||
|
return ArrayUtils.equals(SINGLE_FW_HEADER, wholeFirmwareBytes, SINGLE_FW_HEADER_OFFSET, SINGLE_FW_HEADER_OFFSET + SINGLE_FW_HEADER.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValid() throws IllegalArgumentException {
|
||||||
|
super.checkValid();
|
||||||
|
|
||||||
|
if (wholeFirmwareBytes.length < SINGLE_FW_HEADER.length) {
|
||||||
|
throw new IllegalArgumentException("firmware too small: " + wholeFirmwareBytes.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract int getSupportedMajorVersion();
|
protected abstract int getSupportedMajorVersion();
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,9 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo {
|
|||||||
public boolean isGenerallyCompatibleWith(GBDevice device) {
|
public boolean isGenerallyCompatibleWith(GBDevice device) {
|
||||||
return MiBandConst.MI_1S.equals(device.getHardwareVersion());
|
return MiBandConst.MI_1S.equals(device.getHardwareVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleMiBandFirmware() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
|||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||||
|
|
||||||
public abstract class AbstractMiFirmwareInfo {
|
public abstract class AbstractMiFirmwareInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param wholeFirmwareBytes
|
* @param wholeFirmwareBytes
|
||||||
* @return
|
* @return
|
||||||
@ -20,6 +23,7 @@ public abstract class AbstractMiFirmwareInfo {
|
|||||||
throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?).");
|
throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?).");
|
||||||
}
|
}
|
||||||
if (candidates.length == 1) {
|
if (candidates.length == 1) {
|
||||||
|
candidates[0].checkValid();
|
||||||
return candidates[0];
|
return candidates[0];
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices");
|
throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices");
|
||||||
@ -58,6 +62,8 @@ public abstract class AbstractMiFirmwareInfo {
|
|||||||
|
|
||||||
protected abstract boolean isGenerallySupportedFirmware();
|
protected abstract boolean isGenerallySupportedFirmware();
|
||||||
|
|
||||||
|
protected abstract boolean isHeaderValid();
|
||||||
|
|
||||||
public abstract boolean isGenerallyCompatibleWith(GBDevice device);
|
public abstract boolean isGenerallyCompatibleWith(GBDevice device);
|
||||||
|
|
||||||
public @NonNull byte[] getFirmwareBytes() {
|
public @NonNull byte[] getFirmwareBytes() {
|
||||||
@ -72,12 +78,9 @@ public abstract class AbstractMiFirmwareInfo {
|
|||||||
throw new IllegalArgumentException("bad firmware version: " + version);
|
throw new IllegalArgumentException("bad firmware version: " + version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSingleMiBandFirmware() {
|
public abstract boolean isSingleMiBandFirmware();
|
||||||
// TODO: not sure if this is a correct check!
|
|
||||||
if ((wholeFirmwareBytes[7] & 255) != 1) {
|
public void checkValid() throws IllegalArgumentException {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AbstractMiFirmwareInfo getFirst() {
|
public AbstractMiFirmwareInfo getFirst() {
|
||||||
|
@ -5,6 +5,8 @@ import android.support.annotation.Nullable;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FW1 is Mi Band firmware
|
* FW1 is Mi Band firmware
|
||||||
* FW2 is heartrate firmware
|
* FW2 is heartrate firmware
|
||||||
@ -12,6 +14,14 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class);
|
||||||
|
|
||||||
|
private static final byte[] DOUBLE_FW_HEADER = new byte[] {
|
||||||
|
(byte)0x78,
|
||||||
|
(byte)0x75,
|
||||||
|
(byte)0x63,
|
||||||
|
(byte)0x6b
|
||||||
|
};
|
||||||
|
private static final int DOUBLE_FW_HEADER_OFFSET = 0;
|
||||||
|
|
||||||
private final Mi1SFirmwareInfoFW1 fw1Info;
|
private final Mi1SFirmwareInfoFW1 fw1Info;
|
||||||
private final Mi1SFirmwareInfoFW2 fw2Info;
|
private final Mi1SFirmwareInfoFW2 fw2Info;
|
||||||
|
|
||||||
@ -21,6 +31,33 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
|||||||
fw2Info = new Mi1SFirmwareInfoFW2(wholeFirmwareBytes);
|
fw2Info = new Mi1SFirmwareInfoFW2(wholeFirmwareBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isHeaderValid() {
|
||||||
|
// TODO: not sure if this is a correct check!
|
||||||
|
return ArrayUtils.equals(DOUBLE_FW_HEADER, wholeFirmwareBytes, DOUBLE_FW_HEADER_OFFSET, DOUBLE_FW_HEADER_OFFSET + DOUBLE_FW_HEADER.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValid() throws IllegalArgumentException {
|
||||||
|
super.checkValid();
|
||||||
|
int firstEndIndex = getFirst().getFirmwareOffset() + getFirst().getFirmwareLength();
|
||||||
|
if (getSecond().getFirmwareOffset() < firstEndIndex) {
|
||||||
|
throw new IllegalArgumentException("Invalid firmware offsets/lengths: " + getLengthsOffsetsString());
|
||||||
|
}
|
||||||
|
int secondEndIndex = getSecond().getFirmwareOffset();
|
||||||
|
if (wholeFirmwareBytes.length < firstEndIndex || wholeFirmwareBytes.length < secondEndIndex) {
|
||||||
|
throw new IllegalArgumentException("Invalid firmware size, or invalid offsets/lengths: " + getLengthsOffsetsString());
|
||||||
|
}
|
||||||
|
if (getSecond().getFirmwareOffset() < firstEndIndex) {
|
||||||
|
throw new IllegalArgumentException("Invalid firmware, second fw starts before first fw ends: " + firstEndIndex + "," + getSecond().getFirmwareOffset());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getLengthsOffsetsString() {
|
||||||
|
return getFirst().getFirmwareOffset() + "," + getFirst().getFirmwareLength()
|
||||||
|
+ "; "
|
||||||
|
+ getSecond().getFirmwareOffset() + "," + getSecond().getFirmwareLength();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractMiFirmwareInfo getFirst() {
|
public AbstractMiFirmwareInfo getFirst() {
|
||||||
return fw1Info;
|
return fw1Info;
|
||||||
@ -44,10 +81,11 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isGenerallySupportedFirmware() {
|
protected boolean isGenerallySupportedFirmware() {
|
||||||
if (isSingleMiBandFirmware()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
if (!isHeaderValid()) {
|
||||||
|
LOG.info("unrecognized header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return fw1Info.isGenerallySupportedFirmware()
|
return fw1Info.isGenerallySupportedFirmware()
|
||||||
&& fw2Info.isGenerallySupportedFirmware()
|
&& fw2Info.isGenerallySupportedFirmware()
|
||||||
&& fw1Info.getFirmwareBytes().length > 0
|
&& fw1Info.getFirmwareBytes().length > 0
|
||||||
|
@ -17,6 +17,11 @@ public class Mi1SFirmwareInfoFW1 extends AbstractMi1SFirmwareInfo {
|
|||||||
super(wholeFirmwareBytes);
|
super(wholeFirmwareBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isHeaderValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getFirmwareOffset() {
|
public int getFirmwareOffset() {
|
||||||
return (wholeFirmwareBytes[12] & 255) << 24
|
return (wholeFirmwareBytes[12] & 255) << 24
|
||||||
|
@ -16,6 +16,11 @@ public class Mi1SFirmwareInfoFW2 extends AbstractMi1SFirmwareInfo {
|
|||||||
super(wholeFirmwareBytes);
|
super(wholeFirmwareBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isHeaderValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getFirmwareOffset() {
|
public int getFirmwareOffset() {
|
||||||
return (wholeFirmwareBytes[26] & 255) << 24
|
return (wholeFirmwareBytes[26] & 255) << 24
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||||
|
|
||||||
|
public class ArrayUtils {
|
||||||
|
/**
|
||||||
|
* Checks the two given arrays for equality, but comparing only a subset of the second
|
||||||
|
* array with the whole first array.
|
||||||
|
* @param first the whole array to compare against
|
||||||
|
* @param second the array, of which a subset shall be compared against the whole first array
|
||||||
|
* @param secondStartIndex the start index (inclusive) of the second array from which to start the comparison
|
||||||
|
* @param secondEndIndex the end index (exclusive) of the second array until which to compare
|
||||||
|
* @return whether the first byte array is equal to the specified subset of the second byte array
|
||||||
|
* @throws IllegalArgumentException when one of the arrays is null or start and end index are wrong
|
||||||
|
*/
|
||||||
|
public static boolean equals(byte[] first, byte[] second, int secondStartIndex, int secondEndIndex) {
|
||||||
|
if (first == null) {
|
||||||
|
throw new IllegalArgumentException("first must not be null");
|
||||||
|
}
|
||||||
|
if (second == null) {
|
||||||
|
throw new IllegalArgumentException("second must not be null");
|
||||||
|
}
|
||||||
|
if (secondStartIndex >= secondEndIndex) {
|
||||||
|
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
||||||
|
}
|
||||||
|
if (second.length < secondEndIndex) {
|
||||||
|
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
||||||
|
}
|
||||||
|
if (first.length < secondEndIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int len = secondEndIndex - secondStartIndex;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if (first[i] != second[secondStartIndex + i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ import java.util.Arrays;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
|
|
||||||
@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/")
|
//@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/")
|
||||||
public class FirmwareTest {
|
public class FirmwareTest {
|
||||||
|
|
||||||
private static final long MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1MB
|
private static final long MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1MB
|
||||||
@ -24,6 +25,11 @@ public class FirmwareTest {
|
|||||||
private static final int SINGLE = 1;
|
private static final int SINGLE = 1;
|
||||||
private static final int DOUBLE = 2;
|
private static final int DOUBLE = 2;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupSuite() {
|
||||||
|
getFirmwareDir(); // throws if firmware directory not available
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFirmwareMi1() throws Exception {
|
public void testFirmwareMi1() throws Exception {
|
||||||
byte[] wholeFw = getFirmwareMi();
|
byte[] wholeFw = getFirmwareMi();
|
||||||
@ -124,11 +130,11 @@ public class FirmwareTest {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getFirmwareDir() {
|
private static File getFirmwareDir() {
|
||||||
String path = System.getProperty("MiFirmwareDir");
|
String path = System.getProperty("MiFirmwareDir");
|
||||||
Assert.assertNotNull(path);
|
Assert.assertNotNull("You must run this test with -DMiFirmwareDir=/path/to/directory/with/miband/firmwarefiles/", path);
|
||||||
File dir = new File(path);
|
File dir = new File(path);
|
||||||
Assert.assertTrue(dir.isDirectory());
|
Assert.assertTrue("System property MiFirmwareDir should point to a directory continaing the Mi Band firmware files", dir.isDirectory());
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user