CMF Watch Pro: Firmware update

Tested by updating to 11.0.0.57
This commit is contained in:
José Rebelo
2025-06-27 00:05:18 +01:00
parent d0b85358b6
commit 533ecb9ee3
5 changed files with 142 additions and 20 deletions
@@ -473,8 +473,10 @@ public class BleNamesResolver {
mServices.put("16187f00-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi Wear Service - Mi Smart Watch 4C/Redmi Band)");
mServices.put("1314f000-1000-9000-7000-301291e21220", "(Propr: Xiaomi Wear Service - Mi Watch/Mi Watch Color/Mi Watch Color Sport)");
mServices.put("7495fe00-a7f3-424b-92dd-4a006a3aef56", "(Propr: Xiaomi Wear Service - Mi Watch CN)");
//mServices.put("0000fff0-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command");
//mServices.put("02f00000-0000-0000-0000-00000000ffe0", "(Propr: Nothing CMF Data");
mServices.put("0000fff0-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command");
mServices.put("02f00000-0000-0000-0000-00000000ffe0", "(Propr: Nothing CMF Data");
mServices.put("02f00000-0000-0000-0000-00000000fe00", "(Propr: Nothing CMF Firmware");
mServices.put("77d4e67c-2fe2-2334-0d35-9ccd078f529c", "(Propr: Nothing CMF Shell");
mServices.put("9b012401-bc30-ce9a-e111-0f67e491abde", "(Propr: Garmin GFDI V0)");
mServices.put("6a4e2401-667b-11e3-949a-0800200c9a66", "(Propr: Garmin GFDI V1)");
mServices.put("6a4e2800-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML)");
@@ -972,18 +974,22 @@ public class BleNamesResolver {
mCharacteristics.put("6a4e2823-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 3 TX)");
mCharacteristics.put("6a4e2814-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 4 RX)");
mCharacteristics.put("6a4e2824-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 4 TX)");
//mCharacteristics.put("0000fff1-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Read");
//mCharacteristics.put("0000fff2-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Write");
//mCharacteristics.put("02f00000-0000-0000-0000-00000000ffe1", "(Propr: Nothing CMF Data Write");
//mCharacteristics.put("02f00000-0000-0000-0000-00000000ffe2", "(Propr: Nothing CMF Data Read");
//mCharacteristics.put("00010203-0405-0607-0809-0a0b0c0d2b12", "(Propr: Telink OTA Write)");
//mCharacteristics.put("ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd TIME)");
//mCharacteristics.put("ebe0ccc4-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd BATTERY)");
//mCharacteristics.put("ebe0ccbe-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd TEMPERATURE_UNIT)");
//mCharacteristics.put("ebe0ccd8-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd CONN_INTERVAL)");
//mCharacteristics.put("ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY)");
//mCharacteristics.put("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd LIVE_DATA)");
//mCharacteristics.put("ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY_LAST_ID)");
mCharacteristics.put("0000fff1-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Read");
mCharacteristics.put("0000fff2-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Write");
mCharacteristics.put("02f00000-0000-0000-0000-00000000ffe1", "(Propr: Nothing CMF Data Write");
mCharacteristics.put("02f00000-0000-0000-0000-00000000ffe2", "(Propr: Nothing CMF Data Read");
mCharacteristics.put("77d4ff01-2fe2-2334-0d35-9ccd078f529c", "(Propr: Nothing CMF Shell Write");
mCharacteristics.put("77d4ff02-2fe2-2334-0d35-9ccd078f529c", "(Propr: Nothing CMF Shell Read");
mCharacteristics.put("02f00000-0000-0000-0000-00000000ff01", "(Propr: Nothing CMF Firmware Write");
mCharacteristics.put("02f00000-0000-0000-0000-00000000ff02", "(Propr: Nothing CMF Firmware Read");
mCharacteristics.put("00010203-0405-0607-0809-0a0b0c0d2b12", "(Propr: Telink OTA Write)");
mCharacteristics.put("ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd TIME)");
mCharacteristics.put("ebe0ccc4-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd BATTERY)");
mCharacteristics.put("ebe0ccbe-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd TEMPERATURE_UNIT)");
mCharacteristics.put("ebe0ccd8-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd CONN_INTERVAL)");
mCharacteristics.put("ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY)");
mCharacteristics.put("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd LIVE_DATA)");
mCharacteristics.put("ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY_LAST_ID)");
mCharacteristics.put("00000001-0000-3512-2118-0009af100700", "(Propr: Huami Raw Sensor Control)");
mCharacteristics.put("00000002-0000-3512-2118-0009af100700", "(Propr: Huami Raw Sensor Data)");
mCharacteristics.put("00000003-0000-3512-2118-0009af100700", "(Propr: Huami Configuration)");
@@ -185,6 +185,7 @@ public class CmfCharacteristic {
case AUTH_PAIR_REQUEST:
case AUTH_PAIR_REPLY:
case DATA_CHUNK_WRITE_AGPS:
case DATA_CHUNK_WRITE_FIRMWARE:
case DATA_CHUNK_WRITE_WATCHFACE:
return false;
}
@@ -42,13 +42,21 @@ public enum CmfCommand {
CONTACTS_GET(0x00d5, 0x0002),
CONTACTS_SET(0x00d5, 0x0001),
DATA_CHUNK_REQUEST_AGPS(0xffff, 0xa05f),
DATA_CHUNK_REQUEST_FIRMWARE(0xffff, 0xa042),
DATA_CHUNK_REQUEST_WATCHFACE(0xffff, 0xa064),
DATA_CHUNK_WRITE_AGPS(0xffff, 0x905f),
DATA_CHUNK_WRITE_FIRMWARE(0xffff, 0x9042),
DATA_CHUNK_WRITE_WATCHFACE(0xffff, 0x9064),
DATA_TRANSFER_AGPS_FINISH_ACK_1(0xffff, 0xa060),
DATA_TRANSFER_AGPS_FINISH_ACK_2(0xffff, 0x9060),
DATA_TRANSFER_AGPS_INIT_REPLY(0xffff, 0xa05e),
DATA_TRANSFER_AGPS_INIT_REQUEST(0xffff, 0x905e),
DATA_TRANSFER_FIRMWARE_INIT_1_REQUEST(0xffff, 0x9052),
DATA_TRANSFER_FIRMWARE_INIT_1_REPLY(0xffff, 0xa052),
DATA_TRANSFER_FIRMWARE_INIT_2_REQUEST(0xffff, 0x9040),
DATA_TRANSFER_FIRMWARE_INIT_2_REPLY(0xffff, 0xa040),
DATA_TRANSFER_FIRMWARE_FINISH_ACK_1(0xffff, 0xa041),
DATA_TRANSFER_FIRMWARE_FINISH_ACK_2(0xffff, 0x9041),
DATA_TRANSFER_WATCHFACE_FINISH_ACK_1(0xffff, 0xa065),
DATA_TRANSFER_WATCHFACE_FINISH_ACK_2(0xffff, 0x9065),
DATA_TRANSFER_WATCHFACE_INIT_1_REQUEST(0xffff, 0x8052),
@@ -45,7 +45,7 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
@Override
public void onCommand(final CmfCommand cmd, final byte[] payload) {
switch (cmd) {
case DATA_TRANSFER_WATCHFACE_INIT_1_REPLY:
case DATA_TRANSFER_WATCHFACE_INIT_1_REPLY: {
if (payload[0] != 0x01) {
LOG.warn("Got unexpected transfer init 1 reply {}", payload[0]);
fwHelper = null;
@@ -63,7 +63,30 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
buf.array()
);
return;
}
case DATA_TRANSFER_FIRMWARE_INIT_1_REPLY: {
if (payload[0] != 0x01) {
LOG.warn("Got unexpected firmware init 2 reply {}", payload[0]);
fwHelper = null;
return;
}
final ByteBuffer buf = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
// FIXME version a.b.c.d... how to know? this was from 11.0.0.57
buf.put((byte) (0x0b));
buf.put((byte) (0x00));
buf.put((byte) (0x00));
buf.put((byte) (0x39));
mSupport.sendFirmware(
"transfer firmware init request",
CmfCommand.DATA_TRANSFER_FIRMWARE_INIT_2_REQUEST,
buf.array()
);
return;
}
case DATA_TRANSFER_AGPS_INIT_REPLY:
case DATA_TRANSFER_FIRMWARE_INIT_2_REPLY:
case DATA_TRANSFER_WATCHFACE_INIT_2_REPLY:
if (payload[0] != 0x01) {
LOG.warn("Got unexpected transfer 2 init reply {}", payload[0]);
@@ -78,6 +101,11 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
case DATA_TRANSFER_WATCHFACE_FINISH_ACK_1:
handleAck1(CmfCommand.DATA_TRANSFER_WATCHFACE_FINISH_ACK_2, payload);
return;
case DATA_TRANSFER_FIRMWARE_FINISH_ACK_1:
// TODO: Confirm if this is being sent in the right characteristic, although it looks
// like it does not matter, since it restarts right away
handleAck1(CmfCommand.DATA_TRANSFER_FIRMWARE_FINISH_ACK_2, payload);
return;
case DATA_TRANSFER_AGPS_FINISH_ACK_1:
handleAck1(CmfCommand.DATA_TRANSFER_AGPS_FINISH_ACK_2, payload);
return;
@@ -95,6 +123,13 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
}
handleChunkRequest(CmfCommand.DATA_CHUNK_WRITE_WATCHFACE, payload);
return;
case DATA_CHUNK_REQUEST_FIRMWARE:
if (fwHelper == null || !fwHelper.isFirmware()) {
LOG.warn("We are not sending firmware - refusing request");
return;
}
handleChunkRequest(CmfCommand.DATA_CHUNK_WRITE_FIRMWARE, payload);
return;
}
LOG.warn("Got unknown data command {}", cmd);
@@ -123,6 +158,18 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
return;
}
/* FIXME: This is disabled until we figure out how to send the firmware version
if (fwHelper.isFirmware()) {
mSupport.sendCommand(
"transfer firmware init request",
CmfCommand.DATA_TRANSFER_FIRMWARE_INIT_1_REQUEST,
(byte) 0xa5
);
return;
}
*/
LOG.warn("Unsupported fwHelper for {}", fwHelper.getUri());
fwHelper = null;
}
@@ -137,11 +184,19 @@ public class CmfDataUploader implements CmfCharacteristic.Handler {
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunk offset " + offset);
updateProgress(builder, progress, true);
mSupport.sendData(
"transfer watchface init request",
commandReply,
ArrayUtils.subarray(fwHelper.getBytes(), offset, offset + length)
);
if (commandReply == CmfCommand.DATA_CHUNK_WRITE_FIRMWARE) {
mSupport.sendFirmware(
"send firmware chunk",
commandReply,
ArrayUtils.subarray(fwHelper.getBytes(), offset, offset + length)
);
} else {
mSupport.sendData(
"send data chunk",
commandReply,
ArrayUtils.subarray(fwHelper.getBytes(), offset, offset + length)
);
}
}
private void handleAck1(final CmfCommand commandReply, final byte[] payload) {
@@ -94,6 +94,10 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
public static final UUID UUID_CHARACTERISTIC_CMF_SHELL_WRITE = UUID.fromString("77d4ff01-2fe2-2334-0d35-9ccd078f529c");
public static final UUID UUID_CHARACTERISTIC_CMF_SHELL_READ = UUID.fromString("77d4ff02-2fe2-2334-0d35-9ccd078f529c");
public static final UUID UUID_SERVICE_CMF_FIRMWARE = UUID.fromString("02f00000-0000-0000-0000-00000000fe00");
public static final UUID UUID_CHARACTERISTIC_CMF_FIRMWARE_WRITE = UUID.fromString("02f00000-0000-0000-0000-00000000ff01");
public static final UUID UUID_CHARACTERISTIC_CMF_FIRMWARE_READ = UUID.fromString("02f00000-0000-0000-0000-00000000ff02");
// An a5 byte is used a lot in single payloads, probably as a "proof of encryption"?
public static final byte A5 = (byte) 0xa5;
@@ -101,6 +105,10 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
private CmfCharacteristic characteristicCommandWrite;
private CmfCharacteristic characteristicDataRead;
private CmfCharacteristic characteristicDataWrite;
@Nullable
private CmfCharacteristic characteristicFirmwareRead;
@Nullable
private CmfCharacteristic characteristicFirmwareWrite;
private final byte[] authRandom1 = new byte[16];
private final byte[] authAppSecret = new byte[16];
@@ -116,12 +124,14 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
addSupportedService(UUID_SERVICE_CMF_CMD);
addSupportedService(UUID_SERVICE_CMF_DATA);
addSupportedService(UUID_SERVICE_CMF_SHELL);
addSupportedService(UUID_SERVICE_CMF_FIRMWARE);
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
builder.setUpdateState(getDevice(), GBDevice.State.INITIALIZING, getContext());
@@ -164,18 +174,35 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
LOG.warn("Characteristic shell read is null");
}
final BluetoothGattCharacteristic btCharacteristicFirmwareWrite = getCharacteristic(UUID_CHARACTERISTIC_CMF_FIRMWARE_WRITE);
if (btCharacteristicFirmwareWrite == null) {
LOG.warn("Characteristic firmware write is null");
}
final BluetoothGattCharacteristic btCharacteristicFirmwareRead = getCharacteristic(UUID_CHARACTERISTIC_CMF_FIRMWARE_READ);
if (btCharacteristicFirmwareRead == null) {
LOG.warn("Characteristic firmware read is null");
}
dataUploader = new CmfDataUploader(this);
characteristicCommandRead = new CmfCharacteristic(btCharacteristicCommandRead, this);
characteristicCommandWrite = new CmfCharacteristic(btCharacteristicCommandWrite, null);
characteristicDataRead = new CmfCharacteristic(btCharacteristicDataRead, dataUploader);
characteristicDataWrite = new CmfCharacteristic(btCharacteristicDataWrite, null);
if (btCharacteristicFirmwareRead != null && btCharacteristicFirmwareWrite != null) {
characteristicFirmwareRead = new CmfCharacteristic(btCharacteristicFirmwareRead, dataUploader);
characteristicFirmwareWrite = new CmfCharacteristic(btCharacteristicFirmwareWrite, null);
}
builder.notify(btCharacteristicCommandRead, true);
builder.notify(btCharacteristicDataRead, true);
if (btCharacteristicShellRead != null) {
builder.notify(btCharacteristicShellRead, true);
}
if (btCharacteristicFirmwareRead != null) {
builder.notify(btCharacteristicFirmwareRead, true);
}
builder.setUpdateState(getDevice(), GBDevice.State.AUTHENTICATING, getContext());
@@ -186,6 +213,12 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
characteristicCommandWrite.setSessionKey(secretKey);
characteristicDataRead.setSessionKey(secretKey);
characteristicDataWrite.setSessionKey(secretKey);
if (characteristicFirmwareRead != null) {
characteristicFirmwareRead.setSessionKey(secretKey);
}
if (characteristicFirmwareWrite != null) {
characteristicFirmwareWrite.setSessionKey(secretKey);
}
sendCommand(builder, CmfCommand.AUTH_PHONE_NAME, ArrayUtils.addAll(new byte[]{A5}, Build.MODEL.getBytes(StandardCharsets.UTF_8)));
} else if (btCharacteristicShellWrite != null) {
@@ -221,6 +254,9 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
} else if (characteristicUUID.equals(characteristicDataRead.getCharacteristicUUID())) {
characteristicDataRead.onCharacteristicChanged(value);
return true;
} else if (characteristicFirmwareRead != null && characteristicUUID.equals(characteristicFirmwareRead.getCharacteristicUUID())) {
characteristicFirmwareRead.onCharacteristicChanged(value);
return true;
} else if (characteristicUUID.equals(UUID_CHARACTERISTIC_CMF_SHELL_READ)) {
handleShellCommand(value);
return true;
@@ -255,6 +291,10 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
}
switch (cmd) {
case DATA_TRANSFER_FIRMWARE_INIT_1_REPLY:
// This one comes in the command characteristic because of reasons
dataUploader.onCommand(cmd, payload);
return;
case AUTH_PAIR_REPLY:
final byte[] authRandom2 = ArrayUtils.subarray(payload, 0, 16);
final byte[] signedAuthRandom2 = ArrayUtils.subarray(payload, 16, 48);
@@ -326,6 +366,12 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
characteristicCommandWrite.setSessionKey(sessionKey);
characteristicDataRead.setSessionKey(sessionKey);
characteristicDataWrite.setSessionKey(sessionKey);
if (characteristicFirmwareRead != null) {
characteristicFirmwareRead.setSessionKey(sessionKey);
}
if (characteristicFirmwareWrite != null) {
characteristicFirmwareWrite.setSessionKey(sessionKey);
}
} catch (final Exception e) {
LOG.error("Failed to compute session key from auth nonce", e);
return;
@@ -452,6 +498,12 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen
builder.queue(getQueue());
}
public void sendFirmware(final String taskName, final CmfCommand cmd, final byte... payload) {
final TransactionBuilder builder = createTransactionBuilder(taskName);
characteristicFirmwareWrite.sendCommand(builder, cmd, payload);
builder.queue(getQueue());
}
private void handleShellCommand(final byte[] bytes) {
final String shellCommand = new String(bytes).strip();
if (!shellCommand.startsWith("GETSECRET:")) {