Gate firmware update features by minimum firmware version

- NRF DFU (BLE OTA via Nordic DFU) and UF2 mass-storage work with the app's
  existing minimum firmware (2.5.18) — both use enterDfuModeRequest which has
  been in the firmware long before that version, no extra gate needed.

- ESP32 BIN OTA (BLE/WiFi) requires AdminMessage.OTAEvent with otaHash, which
  was added to the Meshtastic firmware in 2.7.10. Gate these features at 2.7.10:
  • FirmwareRow: show an orange "Requires 2.7.10+" tag and disable the Install
    button for .bin files when the connected device is below 2.7.10.
  • ESP32OTAIntroSheet: show a firmware-update-required warning banner and
    disable the "I Know What I'm Doing" BLE/WiFi OTA buttons when firmware < 2.7.10.

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/ac79356b-8f0b-4297-973b-ee00f5a960d6

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-18 19:16:04 +00:00 committed by GitHub
parent 7718132c6f
commit 23ebe47422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 27 deletions

View file

@ -14,18 +14,26 @@ struct ESP32OTAIntroSheet: View {
case intro
case updater
}
/// ESP32 OTA (BLE/WiFi) requires AdminMessage.OTAEvent with otaHash for authentication,
/// added to Meshtastic firmware in 2.7.10.
private let minimumOTAVersion = "2.7.10"
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var context
let binFileURL: URL
@State var showWifiUpdater = false
@State var debugHost: String = ""
@State var showBLEUpdater = false
/// True when the connected device's firmware supports the OTAEvent protocol.
private var firmwareSupportsOTA: Bool {
accessoryManager.checkIsVersionSupported(forVersion: minimumOTAVersion)
}
var body: some View {
NavigationStack {
List {
@ -33,14 +41,14 @@ struct ESP32OTAIntroSheet: View {
VStack(alignment: .leading, spacing: 12) {
Label("Desktop Recommended", systemImage: "desktopcomputer")
.font(.headline)
Text("The recommended way to update ESP32 devices is using the **Web Flasher** on a desktop computer (Chrome-based browser).")
.fixedSize(horizontal: false, vertical: true)
Text("The **Web Flasher** does not support updating on this device or over USB or BLE.")
.font(.caption)
.foregroundStyle(.secondary)
Link(destination: URL(string: "https://flash.meshtastic.org")!) {
HStack {
Text("Open Web Flasher")
@ -51,31 +59,49 @@ struct ESP32OTAIntroSheet: View {
.buttonStyle(.bordered)
.controlSize(.regular)
}.listRowBackground(Color(UIColor.tertiarySystemBackground))
} footer: {
Color.clear.frame(height: 5)
}
if !firmwareSupportsOTA {
Section {
HStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
VStack(alignment: .leading, spacing: 4) {
Text("Firmware Update Required")
.font(.subheadline.bold())
Text("ESP32 OTA updating requires firmware \(minimumOTAVersion) or later. Update your device firmware using the Web Flasher first.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(.vertical, 4)
}
}
switch OTAMode {
case .wifi:
Section {
VStack(alignment: .leading, spacing: 12) {
Label("WiFi OTA Updating", systemImage: "wifi")
.font(.headline)
HStack(alignment: .top, spacing: 12) {
Image(systemName: "lock.shield")
.font(.title2)
.foregroundStyle(.blue)
Text("Advanced Users Only.")
.font(.callout)
}
Text("If you device has the proper updater loaded into the OTA_1 partition, you can attempt to use the WiFi update process.")
.font(.caption)
.foregroundStyle(.secondary)
Button(role: .destructive) {
self.showWifiUpdater = true
} label: {
@ -84,7 +110,8 @@ struct ESP32OTAIntroSheet: View {
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(maxWidth: .infinity)
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
.cornerRadius(10)
.disabled(accessoryManager.activeDeviceNum == nil || !firmwareSupportsOTA)
}
.padding()
.listRowBackground(Color(UIColor.tertiarySystemBackground))
@ -93,20 +120,20 @@ struct ESP32OTAIntroSheet: View {
VStack(alignment: .leading, spacing: 12) {
Label("BLE OTA Updating", systemImage: "wifi")
.font(.headline)
HStack(alignment: .top, spacing: 12) {
Image(systemName: "lock.shield")
.font(.title2)
.foregroundStyle(.blue)
Text("Advanced Users Only.")
.font(.callout)
}
Text("If you device has the proper updater loaded into the OTA_1 partition, you can attempt to use the BLE update process.")
.font(.caption)
.foregroundStyle(.secondary)
Button(role: .destructive) {
self.showBLEUpdater = true
} label: {
@ -115,7 +142,8 @@ struct ESP32OTAIntroSheet: View {
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(maxWidth: .infinity)
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
.cornerRadius(10)
.disabled(accessoryManager.activeDeviceNum == nil || !firmwareSupportsOTA)
}
.padding()
.listRowBackground(Color(UIColor.tertiarySystemBackground))

View file

@ -333,12 +333,18 @@ struct FirmwareTagView: View {
}
private struct FirmwareRow: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject var firmwareFile: FirmwareFile
@State var unsupporedInstallationMessage: Bool = false
@State var showInstallationSheet: FirmwareFile.FirmwareType?
/// ESP32 OTA (BLE/WiFi) requires the AdminMessage.OTAEvent protocol with otaHash,
/// which was added to Meshtastic firmware in 2.7.10.
private let minimumESP32OTAVersion = "2.7.10"
var body: some View {
VStack {
HStack {
@ -350,11 +356,11 @@ private struct FirmwareRow: View {
case .otaZip:
Text("ZIP").font(.caption2)
}
Text("\(firmwareFile.versionId)")
.font(.caption2)
.foregroundColor(.secondary)
switch firmwareFile.releaseType {
case .stable:
FirmwareTagView("STABLE", color: Color.green)
@ -363,13 +369,17 @@ private struct FirmwareRow: View {
case .unlisted:
FirmwareTagView("UNLISTED", color: Color.orange)
}
if firmwareFile.firmwareType == .bin && !accessoryManager.checkIsVersionSupported(forVersion: minimumESP32OTAVersion) {
FirmwareTagView("Requires \(minimumESP32OTAVersion)+", color: .orange)
}
Spacer()
switch firmwareFile.status {
case .downloading:
ProgressView()
case .downloaded:
Button {
switch firmwareFile.firmwareType {
@ -390,6 +400,7 @@ private struct FirmwareRow: View {
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(UIDevice.current.userInterfaceIdiom == .phone ? .small : .regular)
.disabled(firmwareFile.firmwareType == .bin && !accessoryManager.checkIsVersionSupported(forVersion: minimumESP32OTAVersion))
case .notDownloaded:
Button {