mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Pebble: Update firmware install protocol to better match rebble.
1. We should upload all of the firmware before starting the install process. Otherwise this can cause issues with the install sometimes failing 2. Updates the encoding of the upload start. There are different upload start packet structures 3. Encode the install firmwareStart packet correctly to include how big it will be so that the watch can pre-erase the flash region. 4. Send the firmware then resouces to match the order that the pebble app sends the files. 5. remove arbitrary 100ms sleep in the main loop, the state machine blocks on readWithException already.
This commit is contained in:
+51
@@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2025 Benjamin Temple
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
/**
|
||||
* Sent by the Pebble watch in response to a FirmwareUpdateStart system message.
|
||||
* The watch indicates whether it is ready to receive the firmware transfer.
|
||||
*/
|
||||
public class GBDeviceEventFirmwareUpdateStart extends GBDeviceEvent {
|
||||
public static final byte STATUS_STOPPED = 0;
|
||||
public static final byte STATUS_STARTED = 1;
|
||||
public static final byte STATUS_CANCELLED = 2;
|
||||
|
||||
public byte status;
|
||||
|
||||
public GBDeviceEventFirmwareUpdateStart(byte status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(@NonNull final Context context, @NonNull final GBDevice device) {
|
||||
// Stub — handled in PebbleIoThread.evaluateGBDeviceEventPebble
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + "status: " + status;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -33,6 +33,7 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.ZipEntry;
|
||||
@@ -55,7 +56,8 @@ public class PBWReader {
|
||||
}
|
||||
|
||||
static {
|
||||
fwFileTypesMap = new HashMap<>();
|
||||
// LinkedHashMap - preserve insertion order. We should send the firmware blob before the resources
|
||||
fwFileTypesMap = new LinkedHashMap<>();
|
||||
fwFileTypesMap.put("firmware", PebbleProtocol.PUTBYTES_TYPE_FIRMWARE);
|
||||
fwFileTypesMap.put("resources", PebbleProtocol.PUTBYTES_TYPE_SYSRESOURCES);
|
||||
}
|
||||
|
||||
+79
-22
@@ -42,6 +42,7 @@ import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
@@ -54,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppManagem
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble.GBDeviceEventDataLogging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble.GBDeviceEventFirmwareUpdateStart;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PBWReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleInstallable;
|
||||
@@ -93,6 +95,13 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
private boolean mQuit = false;
|
||||
private boolean mIsConnected = false;
|
||||
private boolean mIsInstalling = false;
|
||||
// Tokens for all committed firmware files; INSTALL is sent for all of them together at the end
|
||||
// so the watch doesn't start flashing mid-transfer while we're still uploading resources.
|
||||
private final ArrayList<Integer> mFirmwareCommittedTokens = new ArrayList<>();
|
||||
// Status from FirmwareUpdateStartResponse (0x0a): -1=pending, 0=stopped, 1=started, 2=cancelled.
|
||||
private int mFirmwareStartStatus = -1;
|
||||
// Timestamp (ms) when WAIT_FIRMWARE_START was entered, for recovery-mode timeout.
|
||||
private long mFirmwareStartTimestamp = 0;
|
||||
|
||||
private PBWReader mPBWReader = null;
|
||||
private GBDeviceApp mCurrentlyInstallingApp = null;
|
||||
@@ -261,6 +270,25 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
try {
|
||||
if (mIsInstalling) {
|
||||
switch (mInstallState) {
|
||||
case WAIT_FIRMWARE_START:
|
||||
if (mFirmwareStartStatus == GBDeviceEventFirmwareUpdateStart.STATUS_STARTED) {
|
||||
LOG.info("Watch confirmed firmware update started");
|
||||
mFirmwareStartStatus = -1;
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
continue;
|
||||
} else if (mFirmwareStartStatus == GBDeviceEventFirmwareUpdateStart.STATUS_STOPPED ||
|
||||
mFirmwareStartStatus == GBDeviceEventFirmwareUpdateStart.STATUS_CANCELLED) {
|
||||
LOG.warn("Watch rejected firmware update start, status={}", mFirmwareStartStatus);
|
||||
finishInstall(true);
|
||||
break;
|
||||
} else if (System.currentTimeMillis() - mFirmwareStartTimestamp > 5000) {
|
||||
// No response after 5s — assume recovery mode which never replies.
|
||||
LOG.info("No FirmwareUpdateStartResponse after 5s, proceeding (recovery mode?)");
|
||||
mFirmwareStartStatus = -1;
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case WAIT_SLOT:
|
||||
if (mInstallSlot == -1) {
|
||||
finishInstall(true); // no slots available
|
||||
@@ -320,13 +348,38 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
}
|
||||
break;
|
||||
case UPLOAD_COMPLETE:
|
||||
writeInstallApp(mPebbleProtocol.encodeUploadComplete(mAppInstallToken));
|
||||
if (++mCurrentInstallableIndex < mPebbleInstallables.length) {
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
if (mPBWReader.isFirmware()) {
|
||||
// For firmware, defer INSTALL until all files are committed.
|
||||
// Sending INSTALL early causes the watch to start writing to flash
|
||||
// immediately, stalling mid-transfer while it's busy erasing. The
|
||||
// stock app sends INSTALL for all files only after every file is committed.
|
||||
mFirmwareCommittedTokens.add(mAppInstallToken);
|
||||
if (++mCurrentInstallableIndex < mPebbleInstallables.length) {
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
} else {
|
||||
mInstallState = PebbleAppInstallState.INSTALL_FIRMWARE;
|
||||
}
|
||||
// continue so the state machine advances immediately
|
||||
continue;
|
||||
} else {
|
||||
mInstallState = PebbleAppInstallState.APP_REFRESH;
|
||||
// For apps, INSTALL each file immediately after commit.
|
||||
writeInstallApp(mPebbleProtocol.encodeUploadComplete(mAppInstallToken));
|
||||
if (++mCurrentInstallableIndex < mPebbleInstallables.length) {
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
} else {
|
||||
mInstallState = PebbleAppInstallState.APP_REFRESH;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case INSTALL_FIRMWARE:
|
||||
// All firmware files are committed; now send INSTALL for each so the
|
||||
// watch can write them all to flash in one go.
|
||||
for (int token : mFirmwareCommittedTokens) {
|
||||
writeInstallApp(mPebbleProtocol.encodeUploadComplete(token));
|
||||
}
|
||||
mFirmwareCommittedTokens.clear();
|
||||
mInstallState = PebbleAppInstallState.APP_REFRESH;
|
||||
break;
|
||||
case APP_REFRESH:
|
||||
if (mPBWReader.isFirmware()) {
|
||||
writeInstallApp(mPebbleProtocol.encodeInstallFirmwareComplete());
|
||||
@@ -380,11 +433,6 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warn("exception 1 in run", e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage() != null && (e.getMessage().equals("broken pipe") || e.getMessage().contains("socket closed"))) { //FIXME: this does not feel right
|
||||
LOG.info(e.getMessage());
|
||||
@@ -468,7 +516,11 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
// FIXME: parts are supposed to be generic code
|
||||
private boolean evaluateGBDeviceEventPebble(GBDeviceEvent deviceEvent) {
|
||||
|
||||
if (deviceEvent instanceof GBDeviceEventVersionInfo) {
|
||||
if (deviceEvent instanceof GBDeviceEventFirmwareUpdateStart) {
|
||||
mFirmwareStartStatus = ((GBDeviceEventFirmwareUpdateStart) deviceEvent).status;
|
||||
LOG.info("Got FirmwareUpdateStartResponse: status={}", mFirmwareStartStatus);
|
||||
return true;
|
||||
} else if (deviceEvent instanceof GBDeviceEventVersionInfo) {
|
||||
if (prefs.syncTime()) {
|
||||
LOG.info("syncing time");
|
||||
write(mPebbleProtocol.encodeSetTime());
|
||||
@@ -627,18 +679,18 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
LOG.info("starting firmware installation");
|
||||
mIsInstalling = true;
|
||||
mInstallSlot = 0;
|
||||
writeInstallApp(mPebbleProtocol.encodeInstallFirmwareStart());
|
||||
mInstallState = PebbleAppInstallState.START_INSTALL;
|
||||
|
||||
/*
|
||||
* This is a hack for recovery mode, in which the blocking read has no timeout and the
|
||||
* firmware installation command does not return any ack.
|
||||
* In normal mode we would got at least out of the blocking read call after a while.
|
||||
*
|
||||
*
|
||||
* ... we should really not handle installation from thread that does the blocking read
|
||||
*
|
||||
*/
|
||||
int totalFirmwareBytes = 0;
|
||||
for (PebbleInstallable pi : mPebbleInstallables) {
|
||||
totalFirmwareBytes += pi.getFileSize();
|
||||
}
|
||||
writeInstallApp(mPebbleProtocol.encodeInstallFirmwareStart(totalFirmwareBytes));
|
||||
mFirmwareStartStatus = -1;
|
||||
mFirmwareStartTimestamp = System.currentTimeMillis();
|
||||
mInstallState = PebbleAppInstallState.WAIT_FIRMWARE_START;
|
||||
// GetTime is sent to ensure the blocking read unblocks in recovery mode, where the
|
||||
// watch never sends FirmwareUpdateStartResponse. In normal mode the watch's response
|
||||
// will arrive first and set mFirmwareStartStatus; the GetTime response is then handled
|
||||
// on the next loop iteration.
|
||||
writeInstallApp(mPebbleProtocol.encodeGetTime());
|
||||
} else {
|
||||
mCurrentlyInstallingApp = mPBWReader.getGBDeviceApp();
|
||||
@@ -713,6 +765,9 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
mPBWReader = null;
|
||||
mIsInstalling = false;
|
||||
mCurrentlyInstallingApp = null;
|
||||
mFirmwareCommittedTokens.clear();
|
||||
mFirmwareStartStatus = -1;
|
||||
mFirmwareStartTimestamp = 0;
|
||||
|
||||
if (mFis != null) {
|
||||
try {
|
||||
@@ -781,6 +836,7 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
|
||||
private enum PebbleAppInstallState {
|
||||
UNKNOWN,
|
||||
WAIT_FIRMWARE_START, // waiting for FirmwareUpdateStartResponse from watch (or 5s timeout for recovery mode)
|
||||
WAIT_SLOT,
|
||||
START_INSTALL,
|
||||
WAIT_TOKEN,
|
||||
@@ -788,6 +844,7 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
UPLOAD_COMMIT,
|
||||
WAIT_COMMIT,
|
||||
UPLOAD_COMPLETE,
|
||||
INSTALL_FIRMWARE, // send INSTALL for all committed firmware tokens after all files are transferred
|
||||
APP_REFRESH,
|
||||
}
|
||||
}
|
||||
|
||||
+52
-18
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppManagement;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble.GBDeviceEventFirmwareUpdateStart;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
@@ -234,6 +235,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
private static final byte SYSTEMMESSAGE_FIRMWARE_OUTOFDATE = 5;
|
||||
private static final byte SYSTEMMESSAGE_STOPRECONNECTING = 6;
|
||||
private static final byte SYSTEMMESSAGE_STARTRECONNECTING = 7;
|
||||
private static final byte SYSTEMMESSAGE_FIRMWARESTART_RESPONSE = 0x0a;
|
||||
|
||||
private static final byte PHONEVERSION_REQUEST = 0;
|
||||
private static final byte PHONEVERSION_APPVERSION_MAGIC = 2; // increase this if pebble complains
|
||||
@@ -1538,16 +1540,28 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
|
||||
/* pebble specific install methods */
|
||||
byte[] encodeUploadStart(byte type, int app_id, int size, String filename) {
|
||||
// The watch uses two different INIT packet formats:
|
||||
// - Firmware/recovery/sysresources: type without bit 7, 1-byte bank number
|
||||
// - App binary/resources/worker: type with bit 7 set, 4-byte app slot
|
||||
// - File (language): type as-is, 1-byte slot, optional filename
|
||||
boolean isFirmwareType = (type == PUTBYTES_TYPE_FIRMWARE ||
|
||||
type == PUTBYTES_TYPE_RECOVERY ||
|
||||
type == PUTBYTES_TYPE_SYSRESOURCES);
|
||||
boolean isFileType = (type == PUTBYTES_TYPE_FILE);
|
||||
|
||||
short length;
|
||||
if (type != PUTBYTES_TYPE_FILE) {
|
||||
if (isFileType) {
|
||||
length = (short) 7;
|
||||
if (filename != null) {
|
||||
length += (short) (filename.getBytes().length + 1);
|
||||
}
|
||||
} else if (isFirmwareType) {
|
||||
// 1-byte bank number; type without bit 7
|
||||
length = (short) 7;
|
||||
} else {
|
||||
// App slot: 4-byte; type with bit 7 to select app-init variant
|
||||
length = (short) 10;
|
||||
type |= (byte) 0b10000000;
|
||||
} else {
|
||||
length = (short) 7;
|
||||
}
|
||||
|
||||
if (type == PUTBYTES_TYPE_FILE && filename != null) {
|
||||
length += (short) ((short) filename.getBytes().length + 1);
|
||||
}
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
||||
@@ -1558,16 +1572,16 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
buf.putInt(size);
|
||||
buf.put(type);
|
||||
|
||||
if (type != PUTBYTES_TYPE_FILE) {
|
||||
buf.putInt(app_id);
|
||||
} else {
|
||||
// slot
|
||||
if (isFileType) {
|
||||
buf.put((byte) app_id);
|
||||
}
|
||||
|
||||
if (type == PUTBYTES_TYPE_FILE && filename != null) {
|
||||
buf.put(filename.getBytes());
|
||||
buf.put((byte) 0);
|
||||
if (filename != null) {
|
||||
buf.put(filename.getBytes());
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
} else if (isFirmwareType) {
|
||||
buf.put((byte) app_id); // 1-byte bank number
|
||||
} else {
|
||||
buf.putInt(app_id); // 4-byte app slot
|
||||
}
|
||||
|
||||
return buf.array();
|
||||
@@ -1632,8 +1646,20 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
|
||||
}
|
||||
|
||||
byte[] encodeInstallFirmwareStart() {
|
||||
return encodeSystemMessage(SYSTEMMESSAGE_FIRMWARESTART);
|
||||
byte[] encodeInstallFirmwareStart(int totalBytes) {
|
||||
// FirmwareUpdateStart includes bytesAlreadyTransferred + bytesToSend
|
||||
// so the watch knows how much data is coming and can pre-erase the full flash region.
|
||||
final short LENGTH_FIRMWARESTART = 10; // command(1) + messageType(1) + alreadyTransferred(4LE) + bytesToSend(4LE)
|
||||
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_FIRMWARESTART);
|
||||
buf.order(ByteOrder.BIG_ENDIAN);
|
||||
buf.putShort(LENGTH_FIRMWARESTART);
|
||||
buf.putShort(ENDPOINT_SYSTEMMESSAGE);
|
||||
buf.put((byte) 0); // command
|
||||
buf.put(SYSTEMMESSAGE_FIRMWARESTART);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN); // size fields are little-endian per Pebble protocol
|
||||
buf.putInt(0); // bytesAlreadyTransferred: always 0 (fresh install)
|
||||
buf.putInt(totalBytes);
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
byte[] encodeInstallFirmwareComplete() {
|
||||
@@ -2137,6 +2163,14 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
case SYSTEMMESSAGE_STARTRECONNECTING:
|
||||
LOG.info(ENDPOINT_NAME + ": start reconnecting");
|
||||
break;
|
||||
case SYSTEMMESSAGE_FIRMWARESTART_RESPONSE:
|
||||
if (buf.remaining() >= 1) {
|
||||
byte status = buf.get();
|
||||
LOG.info(ENDPOINT_NAME + ": firmware update start response, status={}", status);
|
||||
return new GBDeviceEventFirmwareUpdateStart(status);
|
||||
}
|
||||
LOG.warn(ENDPOINT_NAME + ": firmware update start response missing status byte");
|
||||
break;
|
||||
default:
|
||||
LOG.info(ENDPOINT_NAME + ": {}", command);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user