refactor(firmware): Simplify ESP32 firmware check (#4272)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2026-01-20 20:42:16 -06:00
committed by GitHub
parent 85a6900b74
commit b73a304452
8 changed files with 37 additions and 247 deletions

View File

@@ -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
}
]
}

View File

@@ -236,7 +236,6 @@ constructor(
}
base.copy(
requiresBootloaderUpgradeForOta = matchedQuirk.requiresBootloaderUpgradeForOta,
supportsUnifiedOta = matchedQuirk.supportsUnifiedOta,
bootloaderInfoUrl = matchedQuirk.infoUrl,
)
} else {

View File

@@ -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",

View File

@@ -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,
)

View File

@@ -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<String>? = null,
)
) {
/** Returns true if the device architecture is ESP32-based. */
val isEsp32Arc: Boolean
get() = architecture.startsWith("esp32", ignoreCase = true)
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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 {