diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java index e22ba5b532..1bad31f65d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java @@ -30,10 +30,23 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; public class GBDeviceEventVersionInfo extends GBDeviceEvent { private static final Logger LOG = LoggerFactory.getLogger(GBDeviceEventVersionInfo.class); + /** + * Extra info key for dual-slot firmware target slot. + * Value is Integer: 0 for slot 0, 1 for slot 1. + */ + public static final String EXTRA_FW_UPDATE_TARGET_SLOT = "pebble_fw_update_target_slot"; + public String fwVersion = "N/A"; public String fwVersion2 = null; public String hwVersion = "N/A"; + /** + * For dual-slot firmware devices (like Pebble Time 2), this indicates which slot + * firmware updates should target. null means not a dual-slot device. + * 0 = update slot 0, 1 = update slot 1 + */ + public Integer fwUpdateTargetSlot = null; + public GBDeviceEventVersionInfo() { if (GBApplication.getContext() != null) { // Only get from context if there is one (eg. not in unit tests) @@ -63,6 +76,9 @@ public class GBDeviceEventVersionInfo extends GBDeviceEvent { if (hwVersion != null) { device.setModel(hwVersion); } + if (fwUpdateTargetSlot != null) { + device.setExtraInfo(EXTRA_FW_UPDATE_TARGET_SLOT, fwUpdateTargetSlot); + } device.sendDeviceUpdateIntent(context); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java index 395891538a..dea34e55c8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java @@ -48,6 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; public class PBWInstallHandler implements InstallHandler { private static final Logger LOG = LoggerFactory.getLogger(PBWInstallHandler.class); @@ -75,9 +76,10 @@ public class PBWInstallHandler implements InstallHandler { } String platformName = PebbleHardware.getPlatformName(device.getModel()); + Integer targetSlot = (Integer) device.getExtraInfo(GBDeviceEventVersionInfo.EXTRA_FW_UPDATE_TARGET_SLOT); try { - mPBWReader = new PBWReader(mUri, mContext, platformName); + mPBWReader = new PBWReader(mUri, mContext, platformName, targetSlot); } catch (FileNotFoundException e) { installActivity.setInfoText("file not found"); installActivity.setInstallEnabled(false); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java index cae4988c98..7d019b85e4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java @@ -75,7 +75,18 @@ public class PBWReader { private JSONObject mAppKeys = null; + /** + * For dual-slot firmware devices, the slot to install to. + * null for non-dual-slot devices, 0 or 1 for dual-slot devices. + */ + private final Integer targetSlot; + public PBWReader(Uri uri, Context context, String platform) throws IOException { + this(uri, context, platform, null); + } + + public PBWReader(Uri uri, Context context, String platform, Integer targetSlot) throws IOException { + this.targetSlot = targetSlot; uriHelper = UriHelper.get(uri, context); if (uriHelper.getFileName().endsWith(".pbl")) { @@ -99,7 +110,11 @@ public class PBWReader { } String platformDir = ""; - if (!uriHelper.getFileName().endsWith(".pbz")) { + boolean isPbzFirmware = uriHelper.getFileName().endsWith(".pbz"); + int firmwareSlotCount = 0; + String firstSlotHwRevision = null; + + if (!isPbzFirmware) { platformDir = determinePlatformDir(uriHelper, platform); if (platform.equals("chalk") && platformDir.equals("")) { @@ -125,7 +140,35 @@ public class PBWReader { try (ZipInputStream zis = new ZipInputStream(uriHelper.openInputStream())) { while ((ze = zis.getNextEntry()) != null) { String fileName = ze.getName(); - if (fileName.equals(platformDir + "manifest.json")) { + + // For .pbz firmware files, check for slot-based manifests (slot0/, slot1/) + // as well as root manifest.json + String slotPrefix = ""; + boolean isManifest = fileName.equals(platformDir + "manifest.json"); + if (isPbzFirmware && !isManifest) { + // Check for dual-slot firmware format (e.g., slot0/manifest.json, slot1/manifest.json) + if (fileName.matches("slot\\d+/manifest\\.json")) { + slotPrefix = fileName.substring(0, fileName.indexOf('/') + 1); + + // If targetSlot is specified, only match that slot's manifest + if (targetSlot != null) { + String expectedPrefix = "slot" + targetSlot + "/"; + if (slotPrefix.equals(expectedPrefix)) { + isManifest = true; + LOG.info("Using target slot {} firmware manifest: {}", targetSlot, fileName); + } else { + LOG.info("Skipping slot manifest {} (target is slot {})", fileName, targetSlot); + } + } else { + // No target slot specified, process all slots (legacy behavior) + isManifest = true; + LOG.info("Found slot-based firmware manifest: " + fileName); + } + } + } + + if (isManifest) { + String manifestSlotPrefix = slotPrefix.isEmpty() ? platformDir : slotPrefix; long bytes = ze.getSize(); if (bytes > 8192) // that should be too much break; @@ -138,17 +181,47 @@ public class PBWReader { String jsonString = baos.toString(); try { JSONObject json = new JSONObject(jsonString); - HashMap fileTypeMap; + HashMap fileTypeMap = null; try { JSONObject firmware = json.getJSONObject("firmware"); fileTypeMap = fwFileTypesMap; isFirmware = true; - hwRevision = firmware.getString("hwrev"); + String slotHwRevision = firmware.getString("hwrev"); + + // For dual-slot firmware, validate consistency + if (!slotPrefix.isEmpty()) { + firmwareSlotCount++; + if (firstSlotHwRevision == null) { + firstSlotHwRevision = slotHwRevision; + hwRevision = slotHwRevision; + } else if (!firstSlotHwRevision.equals(slotHwRevision)) { + isValid = false; + invalidReason = "Dual-slot firmware hwrev mismatch: " + firstSlotHwRevision + " vs " + slotHwRevision; + LOG.error(invalidReason); + // Will exit via invalidReason check below + } + LOG.info("Slot {} firmware hwrev: {}", slotPrefix, slotHwRevision); + } else { + hwRevision = slotHwRevision; + } } catch (JSONException e) { - fileTypeMap = appFileTypesMap; - isFirmware = false; + // For dual-slot .pbz files, all slots must be firmware + if (isPbzFirmware && !slotPrefix.isEmpty()) { + isValid = false; + invalidReason = "Slot " + slotPrefix + " is not firmware (missing firmware object in manifest)"; + LOG.error(invalidReason); + } else { + fileTypeMap = appFileTypesMap; + isFirmware = false; + } } + + // Exit early if validation failed + if (invalidReason != null || fileTypeMap == null) { + break; + } + for (Map.Entry entry : fileTypeMap.entrySet()) { try { JSONObject jo = json.getJSONObject(entry.getKey()); @@ -156,8 +229,8 @@ public class PBWReader { int size = jo.getInt("size"); long crc = jo.getLong("crc"); byte type = entry.getValue(); - pebbleInstallables.add(new PebbleInstallable(platformDir + name, size, (int) crc, type)); - LOG.info("found file to install: " + platformDir + name); + pebbleInstallables.add(new PebbleInstallable(manifestSlotPrefix + name, size, (int) crc, type)); + LOG.info("found file to install: " + manifestSlotPrefix + name); isValid = true; } catch (JSONException e) { // not fatal @@ -251,6 +324,29 @@ public class PBWReader { } } + // Validate dual-slot firmware + if (isValid && isFirmware && firmwareSlotCount > 0) { + if (targetSlot != null) { + // When target slot is specified, we should have exactly 1 slot + if (firmwareSlotCount == 1) { + LOG.info("Target slot {} firmware ready for install, hwrev '{}'", targetSlot, hwRevision); + } else { + // This shouldn't happen if our filtering is correct + LOG.warn("Expected 1 slot for target {}, found {}", targetSlot, firmwareSlotCount); + } + } else { + // Legacy behavior: when no target slot specified, require both slots + if (firmwareSlotCount < 2) { + isValid = false; + invalidReason = "Dual-slot firmware incomplete: found only " + firmwareSlotCount + " slot(s), expected 2. " + + "Device may need to report its running slot for single-slot install."; + LOG.error(invalidReason); + } else { + LOG.info("Dual-slot firmware validated: both slots present with matching hwrev '{}'", hwRevision); + } + } + } + // Set default invalid reason if still not valid and no specific reason if (!isValid && invalidReason == null) { if (pebbleInstallables == null || pebbleInstallables.isEmpty()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index 7bb3a10d2e..1beb93ce88 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -608,9 +608,10 @@ class PebbleIoThread extends GBDeviceIoThread { } String platformName = PebbleHardware.getPlatformName(gbDevice.getModel()); + Integer targetSlot = (Integer) gbDevice.getExtraInfo(GBDeviceEventVersionInfo.EXTRA_FW_UPDATE_TARGET_SLOT); try { - mPBWReader = new PBWReader(uri, getContext(), platformName); + mPBWReader = new PBWReader(uri, getContext(), platformName, targetSlot); } catch (FileNotFoundException e) { LOG.warn("app file not found", e); return; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 08edcb3490..344b0c2c3c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -293,6 +293,11 @@ public class PebbleProtocol extends GBDeviceProtocol { int mFwMajor = 3; boolean isNewEraPebble = false; + + // Dual-slot firmware support + // Bit 2 (0x04) = IsDualSlot, Bit 3 (0x08) = IsSlot0 + private boolean mIsDualSlot = false; + private boolean mIsSlot0 = false; boolean mEnablePebbleKit = false; boolean mAlwaysACKPebbleKit = false; private byte[] screenshotData = null; @@ -2423,6 +2428,16 @@ public class PebbleProtocol extends GBDeviceProtocol { String gitHash = getFixedString(buf, 8); int fwFlags = buf.get(); LOG.info("git hash: {}, flags: {}", gitHash, fwFlags); + + // Extract dual-slot firmware flags + mIsDualSlot = (fwFlags & 0x04) != 0; // Bit 2: IsDualSlot + mIsSlot0 = (fwFlags & 0x08) != 0; // Bit 3: IsSlot0 + if (mIsDualSlot) { + int runningSlot = mIsSlot0 ? 0 : 1; + int targetSlot = mIsSlot0 ? 1 : 0; + versionCmd.fwUpdateTargetSlot = targetSlot; + LOG.info("Dual-slot firmware detected: running slot {}, will update slot {}", runningSlot, targetSlot); + } int hwRev = buf.get() & 0xFF; // Convert to unsigned String codename = PebbleHardware.getCodenameByHardwareId(hwRev); if (codename != null) {