From b73a30445278e5e2f4583806e40ee33240233dff Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:42:16 -0600 Subject: [PATCH] refactor(firmware): Simplify ESP32 firmware check (#4272) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../assets/device_bootloader_ota_quirks.json | 145 ------------------ .../repository/DeviceHardwareRepository.kt | 1 - .../DeviceHardwareRepositoryTest.kt | 13 ++ .../core/model/BootloaderOtaQuirk.kt | 5 - .../meshtastic/core/model/DeviceHardware.kt | 7 +- .../feature/firmware/FirmwareRetriever.kt | 32 ++-- .../feature/firmware/FirmwareRetrieverTest.kt | 79 +--------- .../feature/settings/SettingsViewModel.kt | 2 +- 8 files changed, 37 insertions(+), 247 deletions(-) diff --git a/app/src/main/assets/device_bootloader_ota_quirks.json b/app/src/main/assets/device_bootloader_ota_quirks.json index e41042b9a..960c63101 100644 --- a/app/src/main/assets/device_bootloader_ota_quirks.json +++ b/app/src/main/assets/device_bootloader_ota_quirks.json @@ -17,151 +17,6 @@ "hwModelSlug": "NOMADSTAR_METEOR_PRO", "requiresBootloaderUpgradeForOta": true, "infoUrl": "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/update-nrf52-bootloader/" - }, - { - "hwModel": 12, - "hwModelSlug": "LILYGO_TBEAM_S3_CORE", - "supportsUnifiedOta": true - }, - { - "hwModel": 16, - "hwModelSlug": "TLORA_T3_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 28, - "hwModelSlug": "SENSELORA_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 43, - "hwModelSlug": "HELTEC_V3", - "supportsUnifiedOta": true - }, - { - "hwModel": 44, - "hwModelSlug": "HELTEC_WSL_V3", - "supportsUnifiedOta": true - }, - { - "hwModel": 45, - "hwModelSlug": "BETAFPV_2400_TX", - "supportsUnifiedOta": true - }, - { - "hwModel": 46, - "hwModelSlug": "BETAFPV_900_NANO_TX", - "supportsUnifiedOta": true - }, - { - "hwModel": 48, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER", - "supportsUnifiedOta": true - }, - { - "hwModel": 49, - "hwModelSlug": "HELTEC_WIRELESS_PAPER", - "supportsUnifiedOta": true - }, - { - "hwModel": 50, - "hwModelSlug": "T_DECK", - "supportsUnifiedOta": true - }, - { - "hwModel": 51, - "hwModelSlug": "T_WATCH_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 52, - "hwModelSlug": "PICOMPUTER_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 53, - "hwModelSlug": "HELTEC_HT62", - "supportsUnifiedOta": true - }, - { - "hwModel": 54, - "hwModelSlug": "EBYTE_ESP32_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 55, - "hwModelSlug": "ESP32_S3_PICO", - "supportsUnifiedOta": true - }, - { - "hwModel": 56, - "hwModelSlug": "CHATTER_2", - "supportsUnifiedOta": true - }, - { - "hwModel": 57, - "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0", - "supportsUnifiedOta": true - }, - { - "hwModel": 58, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0", - "supportsUnifiedOta": true - }, - { - "hwModel": 59, - "hwModelSlug": "UNPHONE", - "supportsUnifiedOta": true - }, - { - "hwModel": 61, - "hwModelSlug": "CDEBYTE_EORA_S3", - "supportsUnifiedOta": true - }, - { - "hwModel": 64, - "hwModelSlug": "RADIOMASTER_900_BANDIT_NANO", - "supportsUnifiedOta": true - }, - { - "hwModel": 65, - "hwModelSlug": "HELTEC_CAPSULE_SENSOR_V3", - "supportsUnifiedOta": true - }, - { - "hwModel": 66, - "hwModelSlug": "HELTEC_VISION_MASTER_T190", - "supportsUnifiedOta": true - }, - { - "hwModel": 67, - "hwModelSlug": "HELTEC_VISION_MASTER_E213", - "supportsUnifiedOta": true - }, - { - "hwModel": 68, - "hwModelSlug": "HELTEC_VISION_MASTER_E290", - "supportsUnifiedOta": true - }, - { - "hwModel": 70, - "hwModelSlug": "SENSECAP_INDICATOR", - "supportsUnifiedOta": true - }, - { - "hwModel": 74, - "hwModelSlug": "RADIOMASTER_900_BANDIT", - "supportsUnifiedOta": true - }, - { - "hwModel": 107, - "hwModelSlug": "THINKNODE_M5", - "supportsUnifiedOta": true - }, - { - "hwModel": 110, - "hwModelSlug": "HELTEC_V4", - "supportsUnifiedOta": true } ] } \ No newline at end of file diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt index 95b5abe1d..716decb9c 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt @@ -236,7 +236,6 @@ constructor( } base.copy( requiresBootloaderUpgradeForOta = matchedQuirk.requiresBootloaderUpgradeForOta, - supportsUnifiedOta = matchedQuirk.supportsUnifiedOta, bootloaderInfoUrl = matchedQuirk.infoUrl, ) } else { diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt index b4afe99e3..0d9543557 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt @@ -96,6 +96,19 @@ class DeviceHardwareRepositoryTest { assertEquals("tdeck-pro", result?.platformioTarget) } + @Test + fun `getDeviceHardwareByModel correctly sets isEsp32Arc for ESP32 devices`() = runTest(testDispatcher) { + val hwModel = 50 + val entities = listOf(createEntity(hwModel, "t-deck", "T-Deck").copy(architecture = "esp32-s3")) + + coEvery { localDataSource.getByHwModel(hwModel) } returns entities + every { bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset() } returns emptyList() + + val result = repository.getDeviceHardwareByModel(hwModel).getOrNull() + + assertEquals(true, result?.isEsp32Arc) + } + private fun createEntity(hwModel: Int, target: String, displayName: String) = DeviceHardwareEntity( activelySupported = true, architecture = "esp32-s3", diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/BootloaderOtaQuirk.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/BootloaderOtaQuirk.kt index f3263729f..959779926 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/BootloaderOtaQuirk.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/BootloaderOtaQuirk.kt @@ -30,11 +30,6 @@ data class BootloaderOtaQuirk( * one-time bootloader upgrade (typically via USB) before DFU updates from the app work. */ @SerialName("requiresBootloaderUpgradeForOta") val requiresBootloaderUpgradeForOta: Boolean = false, - /** - * Indicates that the device supports the ESP32 Unified OTA protocol. When true, the app will use the unified OTA - * handler instead of Nordic DFU. - */ - @SerialName("supportsUnifiedOta") val supportsUnifiedOta: Boolean = false, /** Optional URL pointing to documentation on how to update the bootloader. */ @SerialName("infoUrl") val infoUrl: String? = null, ) diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt index 7ae971a94..c651d8d08 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt @@ -38,7 +38,10 @@ data class DeviceHardware( val requiresBootloaderUpgradeForOta: Boolean? = null, /** Optional URL pointing to documentation for upgrading the bootloader. */ val bootloaderInfoUrl: String? = null, - val supportsUnifiedOta: Boolean = false, val supportLevel: Int? = null, val tags: List? = null, -) +) { + /** Returns true if the device architecture is ESP32-based. */ + val isEsp32Arc: Boolean + get() = architecture.startsWith("esp32", ignoreCase = true) +} diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt index d9b51a7ca..a485c1957 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt @@ -53,25 +53,19 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil hardware: DeviceHardware, onProgress: (Float) -> Unit, ): File? { - // Try MCU-generic Unified OTA binary first, as it's the fastest and newest standard. - // However, we skip the generic binary for devices with specialized UI requirements (MUI/TFT/E-Ink) - // because the generic binary often lacks the necessary drivers. - val hasSpecificUi = hardware.hasMui == true || hardware.hasInkHud == true - if (hardware.supportsUnifiedOta && !hasSpecificUi) { - val mcu = hardware.architecture.replace("-", "") - val otaFilename = "mt-$mcu-ota.bin" - retrieve( - release = release, - hardware = hardware, - onProgress = onProgress, - fileSuffix = ".bin", - internalFileExtension = ".bin", - preferredFilename = otaFilename, - ) - ?.let { - return it - } - } + val mcu = hardware.architecture.replace("-", "") + val otaFilename = "mt-$mcu-ota.bin" + retrieve( + release = release, + hardware = hardware, + onProgress = onProgress, + fileSuffix = ".bin", + internalFileExtension = ".bin", + preferredFilename = otaFilename, + ) + ?.let { + return it + } // Fallback to board-specific binary using the now-accurate platformioTarget. return retrieve( diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt index 432bd79c2..f6b6c10da 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt @@ -31,60 +31,6 @@ class FirmwareRetrieverTest { private val fileHandler: FirmwareFileHandler = mockk() private val retriever = FirmwareRetriever(fileHandler) - @Test - fun `retrieveEsp32Firmware uses mt-arch-ota bin when Unified OTA is supported and no screen`() = runTest { - val release = FirmwareRelease(id = "v2.5.0", zipUrl = "https://example.com/esp32.zip") - val hardware = - DeviceHardware( - hwModelSlug = "HELTEC_V3", - platformioTarget = "heltec-v3", - architecture = "esp32-s3", - supportsUnifiedOta = true, - hasMui = false, - hasInkHud = false, - ) - val expectedFile = File("mt-esp32s3-ota.bin") - - coEvery { fileHandler.checkUrlExists(any()) } returns true - coEvery { fileHandler.downloadFile(any(), any(), any()) } returns expectedFile - - val result = retriever.retrieveEsp32Firmware(release, hardware) {} - - assertEquals(expectedFile, result) - coVerify { - fileHandler.checkUrlExists( - "https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-2.5.0/mt-esp32s3-ota.bin", - ) - } - } - - @Test - fun `retrieveEsp32Firmware skips mt-arch-ota bin for devices with MUI`() = runTest { - val release = FirmwareRelease(id = "v2.5.0", zipUrl = "https://example.com/esp32.zip") - val hardware = - DeviceHardware( - hwModelSlug = "T_DECK", - platformioTarget = "tdeck-tft", - architecture = "esp32-s3", - supportsUnifiedOta = true, - hasMui = true, - ) - val expectedFile = File("firmware-tdeck-tft-2.5.0.bin") - - coEvery { fileHandler.checkUrlExists(any()) } returns true - coEvery { fileHandler.downloadFile(any(), any(), any()) } returns expectedFile - - val result = retriever.retrieveEsp32Firmware(release, hardware) {} - - assertEquals(expectedFile, result) - coVerify(exactly = 0) { fileHandler.checkUrlExists(match { it.contains("mt-esp32s3-ota.bin") }) } - coVerify { - fileHandler.checkUrlExists( - "https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-2.5.0/firmware-tdeck-tft-2.5.0.bin", - ) - } - } - @Test fun `retrieveEsp32Firmware falls back to board-specific bin when mt-arch-ota bin is missing`() = runTest { val release = FirmwareRelease(id = "v2.5.0", zipUrl = "https://example.com/esp32.zip") @@ -93,7 +39,6 @@ class FirmwareRetrieverTest { hwModelSlug = "HELTEC_V3", platformioTarget = "heltec-v3", architecture = "esp32-s3", - supportsUnifiedOta = true, hasMui = false, ) val expectedFile = File("firmware-heltec-v3-2.5.0.bin") @@ -122,16 +67,10 @@ class FirmwareRetrieverTest { } @Test - fun `retrieveEsp32Firmware uses legacy filename for devices without Unified OTA`() = runTest { + fun `retrieveEsp32Firmware uses Unified OTA path for ESP32`() = runTest { val release = FirmwareRelease(id = "v2.5.0", zipUrl = "https://example.com/esp32.zip") - val hardware = - DeviceHardware( - hwModelSlug = "TLORA_V2", - platformioTarget = "tlora-v2", - architecture = "esp32", - supportsUnifiedOta = false, - ) - val expectedFile = File("firmware-tlora-v2-2.5.0.bin") + val hardware = DeviceHardware(hwModelSlug = "TLORA_V2", platformioTarget = "tlora-v2", architecture = "esp32") + val expectedFile = File("mt-esp32-ota.bin") coEvery { fileHandler.checkUrlExists(any()) } returns true coEvery { fileHandler.downloadFile(any(), any(), any()) } returns expectedFile @@ -141,23 +80,15 @@ class FirmwareRetrieverTest { assertEquals(expectedFile, result) coVerify { fileHandler.checkUrlExists( - "https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-2.5.0/firmware-tlora-v2-2.5.0.bin", + "https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-2.5.0/mt-esp32-ota.bin", ) } - // Verify we DID NOT check for mt-esp32-ota.bin - coVerify(exactly = 0) { fileHandler.checkUrlExists(match { it.contains("mt-esp32-ota.bin") }) } } @Test fun `retrieveOtaFirmware uses correct zip extension for NRF52`() = runTest { val release = FirmwareRelease(id = "v2.5.0", zipUrl = "https://example.com/nrf52.zip") - val hardware = - DeviceHardware( - hwModelSlug = "RAK4631", - platformioTarget = "rak4631", - architecture = "nrf52840", - supportsUnifiedOta = false, // OTA via DFU zip - ) + val hardware = DeviceHardware(hwModelSlug = "RAK4631", platformioTarget = "rak4631", architecture = "nrf52840") val expectedFile = File("firmware-rak4631-2.5.0-ota.zip") coEvery { fileHandler.checkUrlExists(any()) } returns true diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index 9866f8c18..2fe9d325c 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -133,7 +133,7 @@ constructor( // ESP32 Unified OTA is only supported via BLE or WiFi (TCP), not USB Serial. val isEsp32OtaSupported = - hw?.supportsUnifiedOta == true && capabilities.supportsEsp32Ota && !radioPrefs.isSerial() + hw?.isEsp32Arc == true && capabilities.supportsEsp32Ota && !radioPrefs.isSerial() flow { emit(hw?.requiresDfu == true || isEsp32OtaSupported) } } else {