mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Initial unified Meshtastic-OTA updater support
This commit is contained in:
parent
811bbdfd20
commit
10ffdf9e10
15 changed files with 1552 additions and 381 deletions
|
|
@ -5274,6 +5274,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BLE Device" : {
|
||||
"comment" : "A label displayed under the name of a BLE device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"BLE OTA Updating" : {
|
||||
|
||||
},
|
||||
"BLE Pin must be 6 digits long." : {
|
||||
"localizations" : {
|
||||
|
|
@ -13646,6 +13653,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ESP32 BLE Updater" : {
|
||||
"comment" : "The title of the view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"ESP32 Device Firmware Update" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -13685,6 +13696,10 @@
|
|||
"comment" : "The title of the view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"ESP32 WiFi Updater" : {
|
||||
"comment" : "The title of the view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Ethernet Options" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -14904,6 +14919,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Firmware File" : {
|
||||
|
||||
},
|
||||
"Firmware installation is not supported for this device architecture." : {
|
||||
"comment" : "An alert message displayed when attempting to install firmware on a device that is not supported.",
|
||||
|
|
@ -18234,7 +18252,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"If you device has the WiFi updater loaded into the OTA_1 partition, you can attempt to use the WiFi update process." : {
|
||||
"If you device has the proper updater loaded into the OTA_1 partition, you can attempt to use the BLE update process." : {
|
||||
|
||||
},
|
||||
"If you device has the proper updater loaded into the OTA_1 partition, you can attempt to use the WiFi update process." : {
|
||||
|
||||
},
|
||||
"Ignore MQTT" : {
|
||||
|
|
@ -22772,6 +22793,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Network Location" : {
|
||||
|
||||
},
|
||||
"Network Status Orange" : {
|
||||
"localizations" : {
|
||||
|
|
@ -26368,6 +26392,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Please be sure this is correct before proceeding." : {
|
||||
|
||||
},
|
||||
"Please connect to a device to see firmware updates." : {
|
||||
|
||||
|
|
@ -28514,8 +28541,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Reboot into OTA Update Mode" : {
|
||||
"comment" : "A button that initiates a reboot of the connected device into OTA update mode.",
|
||||
"Reboot into Wifi OTA Update Mode" : {
|
||||
"comment" : "A button label that prompts the user to reboot their device into OTA update mode.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Reboot node?" : {
|
||||
|
|
@ -41797,9 +41824,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"WiFi Firmware Update" : {
|
||||
|
||||
},
|
||||
"WiFi Options" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; };
|
||||
10D109F22E2047D600536CE6 /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F12E2047D600536CE6 /* DatadogSessionReplay */; };
|
||||
10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F32E2047D600536CE6 /* DatadogTrace */; };
|
||||
230A98402EF86A44004D87F1 /* AsyncCentral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230A983F2EF86A44004D87F1 /* AsyncCentral.swift */; };
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */; };
|
||||
230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */; };
|
||||
231251382E3BC96400E6ED07 /* BLEAuthorizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */; };
|
||||
23148E302EE1CCE500F0DB2C /* MeshtasticAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */; };
|
||||
|
|
@ -22,7 +24,7 @@
|
|||
2315D1A02EECB44800E0FAE7 /* UF2MassStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D19F2EECB44800E0FAE7 /* UF2MassStorageView.swift */; };
|
||||
2315D1A52EED94E800E0FAE7 /* FirmwareFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */; };
|
||||
2315D1A82EEF2ED400E0FAE7 /* SwiftDraw in Frameworks */ = {isa = PBXBuildFile; productRef = 2315D1A72EEF2ED400E0FAE7 /* SwiftDraw */; };
|
||||
23196A6E2EF1BA9100B1504B /* Esp32WifiOTAViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23196A6D2EF1BA9100B1504B /* Esp32WifiOTAViewModel.swift */; };
|
||||
23196A6E2EF1BA9100B1504B /* ESP32WifiOTAViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23196A6D2EF1BA9100B1504B /* ESP32WifiOTAViewModel.swift */; };
|
||||
23196C702EF42D3D00B1504B /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23196C6F2EF42D3D00B1504B /* CircularProgressView.swift */; };
|
||||
231A53782E69ADB900216B99 /* NodeFilterParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231A53772E69ADB900216B99 /* NodeFilterParameters.swift */; };
|
||||
231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; };
|
||||
|
|
@ -60,6 +62,10 @@
|
|||
237AEB972E1FE627003B7CE3 /* BLETransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237AEB962E1FE627003B7CE3 /* BLETransport.swift */; };
|
||||
237AEB992E20098B003B7CE3 /* BLEConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237AEB982E20098B003B7CE3 /* BLEConnection.swift */; };
|
||||
237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */; };
|
||||
23825FF52EF6CF0B00C25543 /* NWHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FF42EF6CF0B00C25543 /* NWHost.swift */; };
|
||||
23825FF92EF6D9AA00C25543 /* ESP32WifiOTASheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FF82EF6D9AA00C25543 /* ESP32WifiOTASheet.swift */; };
|
||||
23825FFD2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */; };
|
||||
23825FFF2EF6E79C00C25543 /* ESP32BLEOTASheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */; };
|
||||
2388EC382EDF88E900F6F982 /* NordicDFU in Frameworks */ = {isa = PBXBuildFile; productRef = 2388EC372EDF88E900F6F982 /* NordicDFU */; };
|
||||
2388EC3A2EDF8A1400F6F982 /* DFUModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2388EC392EDF8A1400F6F982 /* DFUModel.swift */; };
|
||||
23A1AFB72E42BD2500E46C96 /* RXTXIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */; };
|
||||
|
|
@ -73,7 +79,8 @@
|
|||
23C2BE272EE9F4BD00F6A997 /* DeviceHardwareEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE262EE9F4BD00F6A997 /* DeviceHardwareEntity.swift */; };
|
||||
23C2BE2A2EEAF96A00F6A997 /* FirmwareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE292EEAF96A00F6A997 /* FirmwareViewModel.swift */; };
|
||||
23C2BE312EEB823900F6A997 /* NRFDFUSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE302EEB823900F6A997 /* NRFDFUSheet.swift */; };
|
||||
23C2BE342EEC3F9600F6A997 /* ESP32DFUSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE332EEC3F9600F6A997 /* ESP32DFUSheet.swift */; };
|
||||
23C2BE342EEC3F9600F6A997 /* ESP32OTAIntroSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE332EEC3F9600F6A997 /* ESP32OTAIntroSheet.swift */; };
|
||||
23C873E32EF9B58C00BFEA7B /* OTAEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C873E22EF9B58C00BFEA7B /* OTAEnums.swift */; };
|
||||
23D316932E5618D2002FA4FB /* AsyncGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D316922E5618D2002FA4FB /* AsyncGate.swift */; };
|
||||
23D9D9392E50DA97005D1C18 /* ResettableTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */; };
|
||||
23DC50BB2EE76D9C0023838A /* URL+fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DC50BA2EE76D9C0023838A /* URL+fetch.swift */; };
|
||||
|
|
@ -358,13 +365,15 @@
|
|||
/* Begin PBXFileReference section */
|
||||
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
|
||||
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
|
||||
230A983F2EF86A44004D87F1 /* AsyncCentral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncCentral.swift; sourceTree = "<group>"; };
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTAViewModel2.swift; sourceTree = "<group>"; };
|
||||
230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = "<group>"; };
|
||||
231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEAuthorizationHelper.swift; sourceTree = "<group>"; };
|
||||
23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAPI.swift; sourceTree = "<group>"; };
|
||||
2315D19C2EECB3D400E0FAE7 /* UTI+UF2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UTI+UF2.swift"; sourceTree = "<group>"; };
|
||||
2315D19F2EECB44800E0FAE7 /* UF2MassStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UF2MassStorageView.swift; sourceTree = "<group>"; };
|
||||
2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareFile.swift; sourceTree = "<group>"; };
|
||||
23196A6D2EF1BA9100B1504B /* Esp32WifiOTAViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Esp32WifiOTAViewModel.swift; sourceTree = "<group>"; };
|
||||
23196A6D2EF1BA9100B1504B /* ESP32WifiOTAViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32WifiOTAViewModel.swift; sourceTree = "<group>"; };
|
||||
23196C6F2EF42D3D00B1504B /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
||||
231A53772E69ADB900216B99 /* NodeFilterParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParameters.swift; sourceTree = "<group>"; };
|
||||
231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -402,6 +411,10 @@
|
|||
237AEB962E1FE627003B7CE3 /* BLETransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLETransport.swift; sourceTree = "<group>"; };
|
||||
237AEB982E20098B003B7CE3 /* BLEConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEConnection.swift; sourceTree = "<group>"; };
|
||||
237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = "<group>"; };
|
||||
23825FF42EF6CF0B00C25543 /* NWHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWHost.swift; sourceTree = "<group>"; };
|
||||
23825FF82EF6D9AA00C25543 /* ESP32WifiOTASheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32WifiOTASheet.swift; sourceTree = "<group>"; };
|
||||
23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTAViewModel.swift; sourceTree = "<group>"; };
|
||||
23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTASheet.swift; sourceTree = "<group>"; };
|
||||
2388EC392EDF8A1400F6F982 /* DFUModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFUModel.swift; sourceTree = "<group>"; };
|
||||
23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RXTXIndicatorView.swift; sourceTree = "<group>"; };
|
||||
23AB0E652EE35E0200AFA09D /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -414,7 +427,8 @@
|
|||
23C2BE262EE9F4BD00F6A997 /* DeviceHardwareEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHardwareEntity.swift; sourceTree = "<group>"; };
|
||||
23C2BE292EEAF96A00F6A997 /* FirmwareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareViewModel.swift; sourceTree = "<group>"; };
|
||||
23C2BE302EEB823900F6A997 /* NRFDFUSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFDFUSheet.swift; sourceTree = "<group>"; };
|
||||
23C2BE332EEC3F9600F6A997 /* ESP32DFUSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32DFUSheet.swift; sourceTree = "<group>"; };
|
||||
23C2BE332EEC3F9600F6A997 /* ESP32OTAIntroSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32OTAIntroSheet.swift; sourceTree = "<group>"; };
|
||||
23C873E22EF9B58C00BFEA7B /* OTAEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTAEnums.swift; sourceTree = "<group>"; };
|
||||
23D316922E5618D2002FA4FB /* AsyncGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncGate.swift; sourceTree = "<group>"; };
|
||||
23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResettableTimer.swift; sourceTree = "<group>"; };
|
||||
23DC50BA2EE76D9C0023838A /* URL+fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+fetch.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -914,6 +928,26 @@
|
|||
path = Accessory;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23825FFA2EF6E6EC00C25543 /* WiFi */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23196A6D2EF1BA9100B1504B /* ESP32WifiOTAViewModel.swift */,
|
||||
23825FF82EF6D9AA00C25543 /* ESP32WifiOTASheet.swift */,
|
||||
);
|
||||
path = WiFi;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23825FFB2EF6E6FB00C25543 /* BLE */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */,
|
||||
23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */,
|
||||
230A983F2EF86A44004D87F1 /* AsyncCentral.swift */,
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */,
|
||||
);
|
||||
path = BLE;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23C2BD272EE87CFD00F6A997 /* Debugging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -927,7 +961,7 @@
|
|||
children = (
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */,
|
||||
2315D19E2EECB42D00E0FAE7 /* U2F Mass Storage */,
|
||||
23C2BE322EEC3F7800F6A997 /* ESP32 DFU */,
|
||||
23C2BE322EEC3F7800F6A997 /* ESP32 OTA */,
|
||||
23C2BE2F2EEB821400F6A997 /* NRF DFU */,
|
||||
23196C712EF42D4300B1504B /* Helpers */,
|
||||
);
|
||||
|
|
@ -943,13 +977,23 @@
|
|||
path = "NRF DFU";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23C2BE322EEC3F7800F6A997 /* ESP32 DFU */ = {
|
||||
23C2BE322EEC3F7800F6A997 /* ESP32 OTA */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23196A6D2EF1BA9100B1504B /* Esp32WifiOTAViewModel.swift */,
|
||||
23C2BE332EEC3F9600F6A997 /* ESP32DFUSheet.swift */,
|
||||
23C873E12EF9B57D00BFEA7B /* Helpers */,
|
||||
23825FFB2EF6E6FB00C25543 /* BLE */,
|
||||
23825FFA2EF6E6EC00C25543 /* WiFi */,
|
||||
23C2BE332EEC3F9600F6A997 /* ESP32OTAIntroSheet.swift */,
|
||||
);
|
||||
path = "ESP32 DFU";
|
||||
path = "ESP32 OTA";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23C873E12EF9B57D00BFEA7B /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23C873E22EF9B58C00BFEA7B /* OTAEnums.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23D9D9312E50DA0E005D1C18 /* Protocols */ = {
|
||||
|
|
@ -1580,6 +1624,7 @@
|
|||
23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */,
|
||||
DD6F65732C6CB80A0053C113 /* View.swift */,
|
||||
23AB0E652EE35E0200AFA09D /* Image.swift */,
|
||||
23825FF42EF6CF0B00C25543 /* NWHost.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1840,11 +1885,13 @@
|
|||
files = (
|
||||
230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */,
|
||||
25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */,
|
||||
23825FFF2EF6E79C00C25543 /* ESP32BLEOTASheet.swift in Sources */,
|
||||
23C2BE252EE9A8E100F6A997 /* SupportedHardwareBadge.swift in Sources */,
|
||||
25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */,
|
||||
259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */,
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */,
|
||||
DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */,
|
||||
23825FFD2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift in Sources */,
|
||||
259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */,
|
||||
259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
|
||||
|
|
@ -1860,6 +1907,7 @@
|
|||
DDD5BB182C2F9C36007E03CA /* OSLogEntryLog.swift in Sources */,
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
|
||||
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift in Sources */,
|
||||
23C2BE2A2EEAF96A00F6A997 /* FirmwareViewModel.swift in Sources */,
|
||||
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
|
||||
|
|
@ -1900,12 +1948,14 @@
|
|||
23C2BE272EE9F4BD00F6A997 /* DeviceHardwareEntity.swift in Sources */,
|
||||
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
|
||||
23196A6E2EF1BA9100B1504B /* Esp32WifiOTAViewModel.swift in Sources */,
|
||||
23196A6E2EF1BA9100B1504B /* ESP32WifiOTAViewModel.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */,
|
||||
23C873E32EF9B58C00BFEA7B /* OTAEnums.swift in Sources */,
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
|
||||
230A98402EF86A44004D87F1 /* AsyncCentral.swift in Sources */,
|
||||
231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */,
|
||||
23AD546D2E2AE9630046E9AB /* AccessoryManager+MQTT.swift in Sources */,
|
||||
25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */,
|
||||
|
|
@ -1969,6 +2019,7 @@
|
|||
DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */,
|
||||
231A53782E69ADB900216B99 /* NodeFilterParameters.swift in Sources */,
|
||||
232ED4C52E2C5EDD009DA392 /* TCPConnection.swift in Sources */,
|
||||
23825FF92EF6D9AA00C25543 /* ESP32WifiOTASheet.swift in Sources */,
|
||||
DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */,
|
||||
BCB35B4F2E5FC42500B04F60 /* MessageNodeIntent.swift in Sources */,
|
||||
DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */,
|
||||
|
|
@ -2102,11 +2153,12 @@
|
|||
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */,
|
||||
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
|
||||
23C2BE342EEC3F9600F6A997 /* ESP32DFUSheet.swift in Sources */,
|
||||
23C2BE342EEC3F9600F6A997 /* ESP32OTAIntroSheet.swift in Sources */,
|
||||
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
|
||||
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
|
||||
233E99BC2D849C8C00CC3A77 /* WindCompactWidget.swift in Sources */,
|
||||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */,
|
||||
23825FF52EF6CF0B00C25543 /* NWHost.swift in Sources */,
|
||||
232ED4C32E2C5E89009DA392 /* TCPTransport.swift in Sources */,
|
||||
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */,
|
||||
D93068D72B8146690066FBC8 /* MessageText.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ extension AccessoryManager {
|
|||
Logger.transport.info("[Accessory] Event stream closed")
|
||||
}
|
||||
self.activeConnection = (device: device, connection: connection)
|
||||
self.activeDeviceNum = device.num
|
||||
} catch let error as CBError where error.code == .peerRemovedPairingInformation {
|
||||
await self.connectionStepper?.cancelCurrentlyExecutingStep(withError: AccessoryError.coreBluetoothError(error), cancelFullProcess: true)
|
||||
}
|
||||
|
|
@ -213,6 +214,9 @@ extension AccessoryManager {
|
|||
do {
|
||||
try await connectionStepper?.run()
|
||||
Logger.transport.debug("🔗 [Connect] ConnectionStepper completed.")
|
||||
} catch AccessoryError.tooManyRetries {
|
||||
try await self.closeConnection()
|
||||
updateState(.discovering)
|
||||
} catch {
|
||||
Logger.transport.error("🔗 [Connect] Error returned by connectionStepper: \(error)")
|
||||
try await self.closeConnection()
|
||||
|
|
@ -352,7 +356,7 @@ actor SequentialSteps {
|
|||
return
|
||||
}
|
||||
isRunning = false
|
||||
return
|
||||
//return
|
||||
throw AccessoryError.tooManyRetries
|
||||
}
|
||||
|
||||
|
|
|
|||
26
Meshtastic/Extensions/NWHost.swift
Normal file
26
Meshtastic/Extensions/NWHost.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// NWHost.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
extension NWEndpoint.Host {
|
||||
/// Returns the underlying string value (domain name or IP address)
|
||||
/// without extra debug formatting.
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .name(let name, _):
|
||||
return name
|
||||
case .ipv4(let ip):
|
||||
return String(describing: ip)
|
||||
case .ipv6(let ip):
|
||||
return String(describing: ip)
|
||||
@unknown default:
|
||||
return String(describing: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,190 +1,190 @@
|
|||
{
|
||||
"files": {
|
||||
"seeed_xiao_nrf52_kit.svg": {
|
||||
"etag": "\"660b2c3bee85adeccdd5de7ea8d06648\""
|
||||
},
|
||||
"heltec-v3-case.svg": {
|
||||
"etag": "\"e935a15ddd7cd116b9c4203f434ff627\""
|
||||
},
|
||||
"rak4631_case.svg": {
|
||||
"etag": "\"d141ca68501d83f3ca19ed74cb7ce12e\""
|
||||
},
|
||||
"diy.svg": {
|
||||
"etag": "\"7b670e81e7aace4814887ba681fc9f5b\""
|
||||
},
|
||||
"station-g2.svg": {
|
||||
"etag": "\"f0a75bb77ddfcd8fa4c080caa018e539\""
|
||||
},
|
||||
"meteor_pro.svg": {
|
||||
"etag": "\"47ba8e4bc6e224fbd3b09401573549dd\""
|
||||
},
|
||||
"thinknode_m2.svg": {
|
||||
"etag": "\"97441ac3a41d23e5e0f4702f5788643d\""
|
||||
},
|
||||
"rak-wismeshtap.svg": {
|
||||
"etag": "\"8c707dda5c384a10822d3ed785aeb411\""
|
||||
},
|
||||
"tdeck_pro.svg": {
|
||||
"etag": "\"6fca0ce5392b390bb7aa690c57ab0fee\""
|
||||
},
|
||||
"wio_tracker_l1_eink.svg": {
|
||||
"etag": "\"9074596ea8f08acacfa0ce2c9a48152f\""
|
||||
},
|
||||
"thinknode_m3.svg": {
|
||||
"etag": "\"9fbe23b50c26a8c0d5e80a1b9e5bef61\""
|
||||
"crowpanel_2_4.svg": {
|
||||
"etag": "\"3aa8b71d6e9d16f82fddde4ba8b472bd\""
|
||||
},
|
||||
"heltec-vision-master-e290.svg": {
|
||||
"etag": "\"71b598c2c125b115663ab2d40abcd154\""
|
||||
},
|
||||
"t-echo.svg": {
|
||||
"etag": "\"bd2db1e3f0764478a9841ff568abc807\""
|
||||
},
|
||||
"muzi_r1_neo.svg": {
|
||||
"etag": "\"d73a20b71a27e530dc6fbe514f3e9d88\""
|
||||
},
|
||||
"nano-g2-ultra.svg": {
|
||||
"etag": "\"82575f89ab2f60ffe6c1e009b19b596e\""
|
||||
},
|
||||
"heltec-mesh-node-t114.svg": {
|
||||
"etag": "\"ca927ce170fba26438c557af0de47a1e\""
|
||||
},
|
||||
"tbeam.svg": {
|
||||
"etag": "\"ad1781f30226fbe36bae1cbad7e85bac\""
|
||||
},
|
||||
"crowpanel_2_8.svg": {
|
||||
"etag": "\"caad57326211a595f18b5f494ae24b59\""
|
||||
},
|
||||
"crowpanel_5_0.svg": {
|
||||
"etag": "\"a2920df06d5335284db85a2016c0c6c6\""
|
||||
},
|
||||
"rpipicow.svg": {
|
||||
"etag": "\"04fd9771add804a62fbfe45b3d360f22\""
|
||||
},
|
||||
"thinknode_m4.svg": {
|
||||
"etag": "\"bf1503cde2927c24cafaaeeb1cada43f\""
|
||||
},
|
||||
"heltec-mesh-node-t114-case.svg": {
|
||||
"etag": "\"ac7c2abd66e7980db365006332d2b6e7\""
|
||||
},
|
||||
"techo_lite.svg": {
|
||||
"etag": "\"42fdf86393b02396e828149f29295239\""
|
||||
},
|
||||
"tlora-t3s3-epaper.svg": {
|
||||
"etag": "\"dfe63532b984fd3f34ce26b38e1f0807\""
|
||||
},
|
||||
"heltec_v4.svg": {
|
||||
"etag": "\"54e84516a04e1276ca385b41c7aa8b8d\""
|
||||
},
|
||||
"rak4631.svg": {
|
||||
"etag": "\"3f19ff501b98598546fb6d6e5db1151c\""
|
||||
},
|
||||
"lilygo-tlora-pager.svg": {
|
||||
"etag": "\"deb184deacb8006da18ae4751d2e0591\""
|
||||
},
|
||||
"heltec-vision-master-t190.svg": {
|
||||
"etag": "\"7f58cc25f93b203c778a3d6d1c6dc53f\""
|
||||
},
|
||||
"muzi_base.svg": {
|
||||
"etag": "\"d82c0733add18e61809c9a2434bf6148\""
|
||||
},
|
||||
"tlora-v2-1-1_8.svg": {
|
||||
"etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\""
|
||||
},
|
||||
"seeed-sensecap-indicator.svg": {
|
||||
"etag": "\"7a0fc63602d8c978b75799032dfda252\""
|
||||
},
|
||||
"t-deck.svg": {
|
||||
"etag": "\"2187caebf4304bb2308c8ee3ca74dd60\""
|
||||
},
|
||||
"promicro.svg": {
|
||||
"etag": "\"d100b5d3aacf51191d7c4a7eb28db231\""
|
||||
},
|
||||
"heltec-ht62-esp32c3-sx1262.svg": {
|
||||
"etag": "\"1f4d07a164cbc2cb99f26e4a05a763ea\""
|
||||
},
|
||||
"rak_3312.svg": {
|
||||
"etag": "\"a2b5c4fdf127868323c8129f84f8691e\""
|
||||
},
|
||||
"rak11200.svg": {
|
||||
"etag": "\"1a0bfda4331a9bfd29722382a787c700\""
|
||||
},
|
||||
"heltec-wireless-tracker.svg": {
|
||||
"etag": "\"bb7143e1b25d1d18d5727baf69a1caed\""
|
||||
},
|
||||
"wio_tracker_l1_case.svg": {
|
||||
"etag": "\"21eccba8adbb33b1df19fe0de79a8734\""
|
||||
},
|
||||
"crowpanel_3_5.svg": {
|
||||
"etag": "\"2d4ee10776f01156dd9570da888be34f\""
|
||||
},
|
||||
"seeed_solar.svg": {
|
||||
"etag": "\"3cc4099ae22ed261b88f1a9f7d235275\""
|
||||
},
|
||||
"tracker-t1000-e.svg": {
|
||||
"etag": "\"b4194c4bb550f8ccbbf205489f37134c\""
|
||||
},
|
||||
"seeed-xiao-s3.svg": {
|
||||
"etag": "\"9d583ddf39288934736d7ac248987524\""
|
||||
},
|
||||
"m5_c6l.svg": {
|
||||
"etag": "\"f17cb7e59a20ccf41243c666cbe54546\""
|
||||
},
|
||||
"tlora-v2-1-1_6.svg": {
|
||||
"etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\""
|
||||
},
|
||||
"rak-wismesh-tap-v2.svg": {
|
||||
"etag": "\"4acc893e184de92446357fcb5bba7812\""
|
||||
},
|
||||
"rak2560.svg": {
|
||||
"etag": "\"da3e309e4f746f0539e13b1f089411e3\""
|
||||
},
|
||||
"pico.svg": {
|
||||
"etag": "\"9f6b3557953065cce6d56ba6e6d48241\""
|
||||
},
|
||||
"wio-tracker-wm1110.svg": {
|
||||
"etag": "\"2dfb221a6a481f957a59b81dfb0dbaf7\""
|
||||
},
|
||||
"thinknode_m1.svg": {
|
||||
"etag": "\"e525d5710fddf72e1626cf35346a6b25\""
|
||||
},
|
||||
"t-watch-s3.svg": {
|
||||
"etag": "\"2e474b5742ec392304c939b4ec63d466\""
|
||||
},
|
||||
"tbeam-s3-core.svg": {
|
||||
"etag": "\"04c0dab7e74a5c1e647567e150136e5b\""
|
||||
},
|
||||
"crowpanel_2_4.svg": {
|
||||
"etag": "\"3aa8b71d6e9d16f82fddde4ba8b472bd\""
|
||||
},
|
||||
"heltec-wireless-paper.svg": {
|
||||
"etag": "\"7a8d8c8e9e712f32ccdb32edcdaebf5e\""
|
||||
"rak4631_case.svg": {
|
||||
"etag": "\"d141ca68501d83f3ca19ed74cb7ce12e\""
|
||||
},
|
||||
"heltec-wsl-v3.svg": {
|
||||
"etag": "\"3ecfe8273cdf0d7dfb04dad6c3fa449a\""
|
||||
},
|
||||
"tlora-t3s3-v1.svg": {
|
||||
"etag": "\"89510451d52482a475e9cc13503f11a6\""
|
||||
"seeed_xiao_nrf52_kit.svg": {
|
||||
"etag": "\"660b2c3bee85adeccdd5de7ea8d06648\""
|
||||
},
|
||||
"crowpanel_7_0.svg": {
|
||||
"etag": "\"c593914e105b75ee978f5ce2e2a27f1c\""
|
||||
},
|
||||
"rak11310.svg": {
|
||||
"etag": "\"0761c4ec6607993e6133aca9634cd42e\""
|
||||
},
|
||||
"heltec-v3.svg": {
|
||||
"etag": "\"0e22f17d2a0cd67159a222eb0a01bed1\""
|
||||
"rak2560.svg": {
|
||||
"etag": "\"da3e309e4f746f0539e13b1f089411e3\""
|
||||
},
|
||||
"heltec-mesh-solar.svg": {
|
||||
"etag": "\"6d3a4f6266a80493f42c0013e30bb31c\""
|
||||
},
|
||||
"tracker-t1000-e.svg": {
|
||||
"etag": "\"b4194c4bb550f8ccbbf205489f37134c\""
|
||||
},
|
||||
"t-watch-s3.svg": {
|
||||
"etag": "\"2e474b5742ec392304c939b4ec63d466\""
|
||||
},
|
||||
"thinknode_m1.svg": {
|
||||
"etag": "\"e525d5710fddf72e1626cf35346a6b25\""
|
||||
},
|
||||
"rak11310.svg": {
|
||||
"etag": "\"0761c4ec6607993e6133aca9634cd42e\""
|
||||
},
|
||||
"heltec-mesh-node-t114.svg": {
|
||||
"etag": "\"ca927ce170fba26438c557af0de47a1e\""
|
||||
},
|
||||
"crowpanel_3_5.svg": {
|
||||
"etag": "\"2d4ee10776f01156dd9570da888be34f\""
|
||||
},
|
||||
"rak-wismeshtap.svg": {
|
||||
"etag": "\"8c707dda5c384a10822d3ed785aeb411\""
|
||||
},
|
||||
"wio_tracker_l1_case.svg": {
|
||||
"etag": "\"21eccba8adbb33b1df19fe0de79a8734\""
|
||||
},
|
||||
"heltec-wireless-tracker.svg": {
|
||||
"etag": "\"bb7143e1b25d1d18d5727baf69a1caed\""
|
||||
},
|
||||
"rak11200.svg": {
|
||||
"etag": "\"1a0bfda4331a9bfd29722382a787c700\""
|
||||
},
|
||||
"lilygo-tlora-pager.svg": {
|
||||
"etag": "\"deb184deacb8006da18ae4751d2e0591\""
|
||||
},
|
||||
"station-g2.svg": {
|
||||
"etag": "\"f0a75bb77ddfcd8fa4c080caa018e539\""
|
||||
},
|
||||
"thinknode_m3.svg": {
|
||||
"etag": "\"9fbe23b50c26a8c0d5e80a1b9e5bef61\""
|
||||
},
|
||||
"m5_c6l.svg": {
|
||||
"etag": "\"f17cb7e59a20ccf41243c666cbe54546\""
|
||||
},
|
||||
"crowpanel_5_0.svg": {
|
||||
"etag": "\"a2920df06d5335284db85a2016c0c6c6\""
|
||||
},
|
||||
"heltec_v4.svg": {
|
||||
"etag": "\"54e84516a04e1276ca385b41c7aa8b8d\""
|
||||
},
|
||||
"wio-tracker-wm1110.svg": {
|
||||
"etag": "\"2dfb221a6a481f957a59b81dfb0dbaf7\""
|
||||
},
|
||||
"rak4631.svg": {
|
||||
"etag": "\"3f19ff501b98598546fb6d6e5db1151c\""
|
||||
},
|
||||
"diy.svg": {
|
||||
"etag": "\"7b670e81e7aace4814887ba681fc9f5b\""
|
||||
},
|
||||
"rpipicow.svg": {
|
||||
"etag": "\"04fd9771add804a62fbfe45b3d360f22\""
|
||||
},
|
||||
"muzi_base.svg": {
|
||||
"etag": "\"d82c0733add18e61809c9a2434bf6148\""
|
||||
},
|
||||
"seeed-xiao-s3.svg": {
|
||||
"etag": "\"9d583ddf39288934736d7ac248987524\""
|
||||
},
|
||||
"heltec-v3-case.svg": {
|
||||
"etag": "\"e935a15ddd7cd116b9c4203f434ff627\""
|
||||
},
|
||||
"nano-g2-ultra.svg": {
|
||||
"etag": "\"82575f89ab2f60ffe6c1e009b19b596e\""
|
||||
},
|
||||
"tbeam.svg": {
|
||||
"etag": "\"ad1781f30226fbe36bae1cbad7e85bac\""
|
||||
},
|
||||
"heltec-wireless-paper.svg": {
|
||||
"etag": "\"7a8d8c8e9e712f32ccdb32edcdaebf5e\""
|
||||
},
|
||||
"promicro.svg": {
|
||||
"etag": "\"d100b5d3aacf51191d7c4a7eb28db231\""
|
||||
},
|
||||
"tlora-v2-1-1_8.svg": {
|
||||
"etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\""
|
||||
},
|
||||
"seeed_solar.svg": {
|
||||
"etag": "\"3cc4099ae22ed261b88f1a9f7d235275\""
|
||||
},
|
||||
"crowpanel_7_0.svg": {
|
||||
"etag": "\"c593914e105b75ee978f5ce2e2a27f1c\""
|
||||
},
|
||||
"wio_tracker_l1_eink.svg": {
|
||||
"etag": "\"9074596ea8f08acacfa0ce2c9a48152f\""
|
||||
},
|
||||
"heltec_mesh_pocket.svg": {
|
||||
"etag": "\"933aafb0ce3a7b0e1faa67e951bc98ea\""
|
||||
},
|
||||
"heltec-vision-master-e213.svg": {
|
||||
"etag": "\"a56c7707865246300bd9e89b1f7155c5\""
|
||||
"crowpanel_2_8.svg": {
|
||||
"etag": "\"caad57326211a595f18b5f494ae24b59\""
|
||||
},
|
||||
"heltec-vision-master-t190.svg": {
|
||||
"etag": "\"7f58cc25f93b203c778a3d6d1c6dc53f\""
|
||||
},
|
||||
"heltec-mesh-node-t114-case.svg": {
|
||||
"etag": "\"ac7c2abd66e7980db365006332d2b6e7\""
|
||||
},
|
||||
"heltec-v3.svg": {
|
||||
"etag": "\"0e22f17d2a0cd67159a222eb0a01bed1\""
|
||||
},
|
||||
"thinknode_m4.svg": {
|
||||
"etag": "\"bf1503cde2927c24cafaaeeb1cada43f\""
|
||||
},
|
||||
"thinknode_m2.svg": {
|
||||
"etag": "\"97441ac3a41d23e5e0f4702f5788643d\""
|
||||
},
|
||||
"tlora-t3s3-v1.svg": {
|
||||
"etag": "\"89510451d52482a475e9cc13503f11a6\""
|
||||
},
|
||||
"tlora-t3s3-epaper.svg": {
|
||||
"etag": "\"dfe63532b984fd3f34ce26b38e1f0807\""
|
||||
},
|
||||
"t-deck.svg": {
|
||||
"etag": "\"2187caebf4304bb2308c8ee3ca74dd60\""
|
||||
},
|
||||
"tbeam-s3-core.svg": {
|
||||
"etag": "\"04c0dab7e74a5c1e647567e150136e5b\""
|
||||
},
|
||||
"pico.svg": {
|
||||
"etag": "\"9f6b3557953065cce6d56ba6e6d48241\""
|
||||
},
|
||||
"rak-wismesh-tap-v2.svg": {
|
||||
"etag": "\"4acc893e184de92446357fcb5bba7812\""
|
||||
},
|
||||
"rak_wismesh_tag.svg": {
|
||||
"etag": "\"257d649982a6689ec7e7c326c0b4dd2f\""
|
||||
},
|
||||
"t-echo.svg": {
|
||||
"etag": "\"bd2db1e3f0764478a9841ff568abc807\""
|
||||
},
|
||||
"tdeck_pro.svg": {
|
||||
"etag": "\"6fca0ce5392b390bb7aa690c57ab0fee\""
|
||||
},
|
||||
"heltec-ht62-esp32c3-sx1262.svg": {
|
||||
"etag": "\"1f4d07a164cbc2cb99f26e4a05a763ea\""
|
||||
},
|
||||
"seeed-sensecap-indicator.svg": {
|
||||
"etag": "\"7a0fc63602d8c978b75799032dfda252\""
|
||||
},
|
||||
"muzi_r1_neo.svg": {
|
||||
"etag": "\"d73a20b71a27e530dc6fbe514f3e9d88\""
|
||||
},
|
||||
"techo_lite.svg": {
|
||||
"etag": "\"42fdf86393b02396e828149f29295239\""
|
||||
},
|
||||
"tlora-v2-1-1_6.svg": {
|
||||
"etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\""
|
||||
},
|
||||
"meteor_pro.svg": {
|
||||
"etag": "\"47ba8e4bc6e224fbd3b09401573549dd\""
|
||||
},
|
||||
"heltec-vision-master-e213.svg": {
|
||||
"etag": "\"a56c7707865246300bd9e89b1f7155c5\""
|
||||
}
|
||||
},
|
||||
"api_hash": "5fcbe7d3ead1dc1156bccfa4747231615d2a8825d2eac8b34f220a3f04a48155"
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
//
|
||||
// ESP32DFUSheet.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Jake Bordens on 12/12/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import Network
|
||||
|
||||
struct ESP32DFUSheet: View {
|
||||
private enum Step {
|
||||
case intro
|
||||
case updater
|
||||
}
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
@StateObject var ota = Esp32WifiOTAViewModel()
|
||||
let binFileURL: URL
|
||||
@State var host: NWEndpoint.Host?
|
||||
@State private var step: Step = .intro
|
||||
|
||||
init(binFileURL: URL) {
|
||||
self.binFileURL = binFileURL
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
switch step {
|
||||
case .intro:
|
||||
VStack(spacing: 24) {
|
||||
|
||||
// MARK: - Info Card
|
||||
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")
|
||||
Image(systemName: "arrow.up.right")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
|
||||
Divider()
|
||||
|
||||
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 WiFi updater loaded into the OTA_1 partition, you can attempt to use the WiFi update process.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Button(role: .destructive) {
|
||||
self.step = .updater
|
||||
} label: {
|
||||
Text("I Know What I'm Doing")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}.padding(.top)
|
||||
.padding()
|
||||
|
||||
case .updater:
|
||||
Text("WiFi Firmware Update")
|
||||
.font(.headline)
|
||||
|
||||
Text("Please do not leave this screen until this process is complete.")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
|
||||
CircularProgressView(progress: ota.progress, isIndeterminate: (ota.otaState == .handshaking), size: 255.0, subtitleText: ota.otaState.rawValue)
|
||||
|
||||
VStack {
|
||||
switch ota.otaState {
|
||||
case .idle:
|
||||
beginUpdateProcessButton()
|
||||
|
||||
case .error:
|
||||
Text("Error: \(ota.errorMessage, default: "Unknown")")
|
||||
|
||||
default:
|
||||
Text("\(ota.statusMessage, default: "")")
|
||||
}
|
||||
}.frame(minHeight: 250.0)
|
||||
.padding()
|
||||
}
|
||||
}.navigationTitle("ESP32 Update")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { // Standard placement for "Done" or "Close"
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}.disabled(![.idle, .success, .error].contains(ota.otaState))
|
||||
}
|
||||
}
|
||||
|
||||
}// Standard Navigation Bar Setup
|
||||
|
||||
.onFirstAppear {
|
||||
if let connection = accessoryManager.activeConnection?.connection as? TCPConnection {
|
||||
self.host = connection.host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func beginUpdateProcessButton() -> some View {
|
||||
Button {
|
||||
let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context)
|
||||
if let connectedNode, let user = connectedNode.user {
|
||||
Task {
|
||||
do {
|
||||
if let host {
|
||||
let device = accessoryManager.activeConnection?.device
|
||||
try await accessoryManager.sendRebootOta(fromUser: user, toUser: user, rebootOtaSeconds: 1)
|
||||
try await accessoryManager.disconnect()
|
||||
await ota.startUpdate(host: host, firmwareUrl: self.binFileURL)
|
||||
if let device {
|
||||
try await Task.sleep(for: .seconds(3))
|
||||
try await accessoryManager.connect(to: device, retries: 5)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.error("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Reboot into OTA Update Mode", systemImage: "square.and.arrow.down")
|
||||
}.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
//
|
||||
// AsyncCentral.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/21/25.
|
||||
//
|
||||
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
|
||||
private let meshtasticOTAServiceId = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B")
|
||||
private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003")
|
||||
private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005")
|
||||
|
||||
enum BLEError: Error {
|
||||
case poweredOff, scanTimeout, connectFailed, serviceMissing, characteristicMissing
|
||||
}
|
||||
|
||||
final class AsyncCentral: NSObject {
|
||||
private var central: CBCentralManager!
|
||||
private var scanContinuation: CheckedContinuation<CBPeripheral, Error>?
|
||||
private var connectContinuation: CheckedContinuation<Void, Error>?
|
||||
private var serviceContinuation: CheckedContinuation<[CBService], Error>?
|
||||
private var characteristicContinuation: CheckedContinuation<[CBCharacteristic], Error>?
|
||||
private var notifyContinuation: CheckedContinuation<Void, Error>?
|
||||
private var writeContinuation: CheckedContinuation<Void, Error>?
|
||||
private var notificationStreams: [CBUUID: AsyncStream<Data>.Continuation] = [:]
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
central = CBCentralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
func waitUntilPoweredOn() async throws {
|
||||
if central.state == .poweredOn { return }
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
self.powerContinuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
private var powerContinuation: CheckedContinuation<Void, Error>?
|
||||
}
|
||||
|
||||
extension AsyncCentral: CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
if central.state == .poweredOn {
|
||||
powerContinuation?.resume()
|
||||
powerContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
||||
advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
scanContinuation?.resume(returning: peripheral)
|
||||
scanContinuation = nil
|
||||
central.stopScan()
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
connectContinuation?.resume()
|
||||
connectContinuation = nil
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
connectContinuation?.resume(throwing: error ?? BLEError.connectFailed)
|
||||
connectContinuation = nil
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
if let error = error { serviceContinuation?.resume(throwing: error) }
|
||||
else { serviceContinuation?.resume(returning: peripheral.services ?? []) }
|
||||
serviceContinuation = nil
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
if let error = error { characteristicContinuation?.resume(throwing: error) }
|
||||
else { characteristicContinuation?.resume(returning: service.characteristics ?? []) }
|
||||
characteristicContinuation = nil
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||||
if let error = error { notifyContinuation?.resume(throwing: error) }
|
||||
else { notifyContinuation?.resume() }
|
||||
notifyContinuation = nil
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
guard error == nil, let data = characteristic.value else { return }
|
||||
notificationStreams[characteristic.uuid]?.yield(data)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
if let error = error {
|
||||
writeContinuation?.resume(throwing: error)
|
||||
} else {
|
||||
writeContinuation?.resume()
|
||||
}
|
||||
writeContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension AsyncCentral {
|
||||
func scan(for service: CBUUID, timeout: TimeInterval = 10) async throws -> CBPeripheral {
|
||||
try await withThrowingTaskGroup(of: CBPeripheral.self) { group in
|
||||
group.addTask {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
self.scanContinuation = cont
|
||||
self.central.scanForPeripherals(withServices: [service], options: nil)
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
||||
throw BLEError.scanTimeout
|
||||
}
|
||||
let result = try await group.next()! // first to finish
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func connect(_ peripheral: CBPeripheral) async throws {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
self.connectContinuation = cont
|
||||
central.connect(peripheral, options: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverServices(_ uuids: [CBUUID], on peripheral: CBPeripheral) async throws -> [CBService] {
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices(uuids)
|
||||
return try await withCheckedThrowingContinuation { cont in
|
||||
self.serviceContinuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
func discoverCharacteristics(_ uuids: [CBUUID], in service: CBService, on peripheral: CBPeripheral) async throws -> [CBCharacteristic] {
|
||||
peripheral.discoverCharacteristics(uuids, for: service)
|
||||
return try await withCheckedThrowingContinuation { cont in
|
||||
self.characteristicContinuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
func setNotify(_ enabled: Bool, for characteristic: CBCharacteristic, on peripheral: CBPeripheral) async throws {
|
||||
peripheral.setNotifyValue(enabled, for: characteristic)
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
self.notifyContinuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
func notifications(for characteristic: CBCharacteristic) -> AsyncStream<Data> {
|
||||
AsyncStream { cont in
|
||||
notificationStreams[characteristic.uuid] = cont
|
||||
}
|
||||
}
|
||||
|
||||
func writeValue(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, on peripheral: CBPeripheral) async throws {
|
||||
if type == .withResponse {
|
||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||
self.writeContinuation = cont
|
||||
peripheral.writeValue(data, for: characteristic, type: type)
|
||||
}
|
||||
} else {
|
||||
peripheral.writeValue(data, for: characteristic, type: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// ESP3BLEOTASheet.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import CoreBluetooth
|
||||
|
||||
|
||||
struct ESP32BLEOTASheet: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@StateObject var ota = ESP32BLEOTAViewModel2()
|
||||
|
||||
// The stuff were updating, and the place we're updating it to
|
||||
let binFileURL: URL
|
||||
@State var peripheral: CBPeripheral?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
VStack {
|
||||
Text("Please do not leave this screen until this process is complete.")
|
||||
.multilineTextAlignment(.center)
|
||||
}.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Firmware File").font(.caption).foregroundColor(.secondary)
|
||||
Text("\(self.binFileURL.lastPathComponent)").font(.caption)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text("BLE Device").font(.caption).foregroundColor(.secondary)
|
||||
Text("\(peripheral?.name, default: "Unknown")").font(.caption)
|
||||
Text("\(peripheral?.identifier, default: "Unknown")").font(.caption)
|
||||
}
|
||||
} footer: {
|
||||
Text("Please be sure this is correct before proceeding.")
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
CircularProgressView(progress: ota.transferProgress / 100.0, isIndeterminate: (ota.otaStatus == .preparing), size: 225.0, subtitleText: ota.otaStatus.rawValue)
|
||||
.frame(minHeight: 250.0)
|
||||
Spacer()
|
||||
}.listRowBackground(Color.clear)
|
||||
VStack {
|
||||
if ota.otaStatus == .idle {
|
||||
beginBLEProcessButton()
|
||||
} else {
|
||||
Text("\(ota.statusMessage)")
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}.listRowBackground(Color.clear)
|
||||
}.listRowSeparator(.hidden)
|
||||
}.navigationTitle("ESP32 BLE Updater")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { // Standard placement for "Done" or "Close"
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}.disabled(![.idle, .completed, .error].contains(ota.otaStatus))
|
||||
}
|
||||
}
|
||||
}.task {
|
||||
if let connection = accessoryManager.activeConnection?.connection as? BLEConnection {
|
||||
self.peripheral = await connection.peripheral
|
||||
}
|
||||
}.interactiveDismissDisabled(true)
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func beginBLEProcessButton() -> some View {
|
||||
Button {
|
||||
let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context)
|
||||
if let connectedNode, let user = connectedNode.user {
|
||||
Task {
|
||||
do {
|
||||
if let peripheral {
|
||||
try await accessoryManager.sendRebootOta(fromUser: user, toUser: user, rebootOtaSeconds: 2)
|
||||
try await accessoryManager.disconnect()
|
||||
|
||||
ota.startOTA(binURL: binFileURL)
|
||||
//ota.startOTA(peripheral: peripheral, binFileURL: binFileURL)
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.error("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Reboot into Wifi OTA Update Mode", systemImage: "square.and.arrow.down")
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}.buttonStyle(.bordered)
|
||||
.controlSize(.large)
|
||||
.disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
//
|
||||
// ESP32BLEOTAViewModel.swift (previously BLEConnection.swift in the DFU app)
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/4/22
|
||||
//
|
||||
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
|
||||
private let meshtasticOTAServiceId = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B")
|
||||
private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") // ESP32 pTxCharacteristic ESP send (notifying)
|
||||
private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005") // ESP32 pOtaCharacteristic ESP write
|
||||
|
||||
private let outOfRangeHeuristics: Set<CBError.Code> = [.unknown, .connectionTimeout, .peripheralDisconnected, .connectionFailed]
|
||||
|
||||
class ESP32BLEOTAViewModel: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
|
||||
var manager: CBCentralManager!
|
||||
var statusCharacteristic: CBCharacteristic?
|
||||
var otaCharacteristic: CBCharacteristic?
|
||||
|
||||
var otaCharacteristicIsNotifying = false
|
||||
var statusCharacteristicIsNotifying = false
|
||||
|
||||
var state = StateBLE.poweredOff
|
||||
|
||||
enum StateBLE {
|
||||
case poweredOff
|
||||
case restoringConnectingPeripheral(CBPeripheral)
|
||||
case restoringConnectedPeripheral(CBPeripheral)
|
||||
case disconnected
|
||||
case scanning
|
||||
case connecting(CBPeripheral, Countdown)
|
||||
case discoveringServices(CBPeripheral, Countdown)
|
||||
case discoveringCharacteristics(CBPeripheral, Countdown)
|
||||
case connected(CBPeripheral)
|
||||
case outOfRange(CBPeripheral)
|
||||
|
||||
var peripheral: CBPeripheral? {
|
||||
switch self {
|
||||
case .poweredOff: return nil
|
||||
case .restoringConnectingPeripheral(let p): return p
|
||||
case .restoringConnectedPeripheral(let p): return p
|
||||
case .disconnected: return nil
|
||||
case .scanning: return nil
|
||||
case .connecting(let p, _): return p
|
||||
case .discoveringServices(let p, _): return p
|
||||
case .discoveringCharacteristics(let p, _): return p
|
||||
case .connected(let p): return p
|
||||
case .outOfRange(let p): return p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used by contentView.swift
|
||||
@Published var name = ""
|
||||
@Published var connected = false
|
||||
@Published var transferProgress : Double = 0.0
|
||||
@Published var chunkCount = 1 // number of chunks to be sent before peripheral needs to accknowledge.
|
||||
@Published var elapsedTime = 0.0
|
||||
@Published var kBPerSecond = 0.0
|
||||
|
||||
// OTA file URL
|
||||
var fileUrl: URL?
|
||||
var desiredPeripheral: CBPeripheral?
|
||||
|
||||
// transfer varibles
|
||||
var dataToSend = Data()
|
||||
var dataBuffer = Data()
|
||||
var chunkSize = 0
|
||||
var dataLength = 0
|
||||
var transferOngoing = true
|
||||
var sentBytes = 0
|
||||
var packageCounter = 0
|
||||
var startTime = 0.0
|
||||
var stopTime = 0.0
|
||||
var firstAcknowledgeFromESP32 = false
|
||||
|
||||
// Initiate CentralManager
|
||||
override init() {
|
||||
super.init()
|
||||
manager = CBCentralManager(delegate: self, queue: .none)
|
||||
manager.delegate = self
|
||||
}
|
||||
|
||||
func startOTA(peripheral: CBPeripheral, binFileURL: URL) {
|
||||
self.desiredPeripheral = peripheral
|
||||
self.fileUrl = binFileURL
|
||||
|
||||
self.startScanning()
|
||||
}
|
||||
|
||||
// CentralManager State updates
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
Logger.services.info("Bluetooth State Updated")
|
||||
switch manager.state {
|
||||
case .unknown:
|
||||
Logger.services.info("Unknown")
|
||||
case .resetting:
|
||||
Logger.services.info("Resetting")
|
||||
case .unsupported:
|
||||
Logger.services.info("Unsupported")
|
||||
case .unauthorized:
|
||||
Logger.services.info("Bluetooth is disabled")
|
||||
case .poweredOff:
|
||||
Logger.services.info("Bluetooth is powered off")
|
||||
case .poweredOn:
|
||||
Logger.services.info("Bluetooth is working properly")
|
||||
@unknown default:
|
||||
Logger.services.info("fatal error")
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery (scanning) and handling of BLE devices in range
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
guard case .scanning = state else { return }
|
||||
|
||||
self.name = peripheral.name ?? "Unknown"
|
||||
Logger.services.info("Discovered \(self.name)")
|
||||
|
||||
// Check if this is the desired peripheral
|
||||
if let desiredPeripheral, desiredPeripheral.identifier != peripheral.identifier {
|
||||
Logger.services.info("This peripheral is not the one we're looking for")
|
||||
}
|
||||
|
||||
manager.stopScan()
|
||||
connect(peripheral: peripheral)
|
||||
}
|
||||
|
||||
// Connection established handler
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
Logger.services.info("Connection suceeded")
|
||||
|
||||
transferOngoing = false
|
||||
|
||||
// Clear the data that we may already have
|
||||
dataToSend.removeAll(keepingCapacity: false)
|
||||
|
||||
// Make sure we get the discovery callbacks
|
||||
peripheral.delegate = self
|
||||
|
||||
if peripheral.statusCharacteristic == nil {
|
||||
discoverServices(peripheral: peripheral)
|
||||
} else {
|
||||
setConnected(peripheral: peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
// Connection failed
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
Logger.services.info("\(Date()) CM DidFailToConnect")
|
||||
state = .disconnected
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
transferOngoing = false
|
||||
Logger.services.info("\(peripheral.name ?? "unknown") disconnected")
|
||||
// Did our currently-connected peripheral just disconnect?
|
||||
if state.peripheral?.identifier == peripheral.identifier {
|
||||
name = ""
|
||||
connected = false
|
||||
// IME the error codes encountered are:
|
||||
// 0 = rebooting the peripheral.
|
||||
// 6 = out of range.
|
||||
if let error = error, (error as NSError).domain == CBErrorDomain,
|
||||
let code = CBError.Code(rawValue: (error as NSError).code),
|
||||
outOfRangeHeuristics.contains(code) {
|
||||
// Try reconnect without setting a timeout in the state machine.
|
||||
// With CB, it's like saying 'please reconnect me at any point
|
||||
// in the future if this peripheral comes back into range'.
|
||||
Logger.services.info("Connection failure, try and reconnect when the device is back in range")
|
||||
manager.connect(peripheral, options: nil)
|
||||
state = .outOfRange(peripheral)
|
||||
} else {
|
||||
// Likely a deliberate unpairing.
|
||||
state = .disconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Peripheral callbacks
|
||||
// -----------------------------------------
|
||||
|
||||
// Discover BLE device service(s)
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
Logger.services.info("Discovered Bluetooth Services")
|
||||
// Ignore services discovered late.
|
||||
guard case .discoveringServices = state else {
|
||||
return
|
||||
}
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
guard peripheral.meshtasticOTAService != nil else {
|
||||
Logger.services.info("Meshtastic OTA service missing")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
// All fine so far, go to next step
|
||||
guard let services = peripheral.services else { return }
|
||||
for service in services {
|
||||
peripheral.discoverCharacteristics(nil, for: service)
|
||||
}
|
||||
|
||||
}
|
||||
// Discover BLE device Service charachteristics
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
Logger.services.info("Discovering Characteristics For Meshtastic OTA Service")
|
||||
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
guard peripheral.statusCharacteristic != nil else {
|
||||
Logger.services.info("\(Date()) Desired characteristic missing")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
guard let characteristics = service.characteristics else {
|
||||
return
|
||||
}
|
||||
|
||||
for characteristic in characteristics {
|
||||
switch characteristic.uuid {
|
||||
|
||||
case statusCharacteristicId:
|
||||
statusCharacteristic = characteristic
|
||||
Logger.services.info("Discovered Status Characteristic: \(self.statusCharacteristic!.uuid.uuidString)")
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
|
||||
case otaCharacteristicId:
|
||||
otaCharacteristic = characteristic
|
||||
Logger.services.info("Discovered OTA Characteristic: \(self.otaCharacteristic!.uuid.uuidString)")
|
||||
peripheral.setNotifyValue(false, for: characteristic)
|
||||
|
||||
default:
|
||||
Logger.services.info("\(Date()) unknown")
|
||||
}
|
||||
}
|
||||
setConnected(peripheral: peripheral)
|
||||
}
|
||||
// The BLE peripheral device sent some notify data. Deal with it!
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didUpdateValueFor")
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
if let data = characteristic.value {
|
||||
// deal with incoming data
|
||||
// First check if the incoming data is one byte length?
|
||||
// if so it's the peripheral acknowledging and telling
|
||||
// us to send another batch of data
|
||||
if data.count == 1 {
|
||||
if !firstAcknowledgeFromESP32 {
|
||||
firstAcknowledgeFromESP32 = true
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
// Logger.services.info("\(Date()) -X-")
|
||||
if transferOngoing {
|
||||
packageCounter = 0
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when .withResponse is used.
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didWriteValueFor")
|
||||
if let error = error {
|
||||
Logger.services.error("\(Date()) Error writing to characteristic: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Callback indicating peripheral notifying state
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didUpdateNotificationStateFor")
|
||||
Logger.services.info("\(Date()) PH characteristic: \(characteristic.uuid.uuidString)")
|
||||
if error == nil {
|
||||
Logger.services.info("\(Date()) Notification Set OK, isNotifying: \(characteristic.isNotifying)")
|
||||
if !characteristic.isNotifying {
|
||||
Logger.services.info("\(Date()) isNotifying is false, set to true again!")
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
} else {
|
||||
if characteristic.uuid == statusCharacteristic?.uuid {
|
||||
statusCharacteristicIsNotifying = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkStartTransfer()
|
||||
}
|
||||
|
||||
func checkStartTransfer() {
|
||||
guard connected else {
|
||||
Logger.services.info("Not connected, cannot start transfer")
|
||||
return
|
||||
}
|
||||
guard statusCharacteristicIsNotifying else {
|
||||
Logger.services.info("Status Characteristic not notifying yet, cannot start transfer")
|
||||
return
|
||||
}
|
||||
guard transferOngoing == false else {
|
||||
Logger.services.info("Transfer already ongoing")
|
||||
return
|
||||
}
|
||||
|
||||
// All set, start the transfer
|
||||
self.sendFile()
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
Functions
|
||||
-------------------------------------------------------------------------*/
|
||||
// Scan for a device with the OTA Service UUID (myDesiredServiceId)
|
||||
func startScanning() {
|
||||
Logger.services.info("Scanning for Meshtastic Devices in OTA Mode")
|
||||
guard manager.state == .poweredOn else {
|
||||
Logger.services.info("Cannot scan, Bluetooth is not powered on")
|
||||
return
|
||||
}
|
||||
manager.scanForPeripherals(withServices: [meshtasticOTAServiceId], options: nil)
|
||||
// state = .scanning(Countdown(seconds: 10, closure: {
|
||||
// self.manager.stopScan()
|
||||
// self.state = .disconnected
|
||||
// Logger.services.info("Scan timed out")
|
||||
// }))
|
||||
state = .scanning
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
Logger.services.info("Disconnect")
|
||||
if let peripheral = state.peripheral {
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
}
|
||||
state = .disconnected
|
||||
connected = false
|
||||
transferOngoing = false
|
||||
}
|
||||
|
||||
// Connect to the device from the scanning
|
||||
func connect(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Connect Button Pushed")
|
||||
if connected {
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
} else {
|
||||
// Connect!
|
||||
manager.connect(peripheral, options: nil)
|
||||
name = String(peripheral.name ?? "unknown")
|
||||
Logger.services.info("Attempting connection to \(self.name)")
|
||||
state = .connecting(peripheral, Countdown(seconds: 10, closure: {
|
||||
self.manager.cancelPeripheralConnection(self.state.peripheral!)
|
||||
self.state = .disconnected
|
||||
self.connected = false
|
||||
Logger.services.info("Attempted connection to \(self.name) timed out")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Discover Services of a device
|
||||
func discoverServices(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Discovering Meshtastic OTA service")
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices([meshtasticOTAServiceId])
|
||||
state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: {
|
||||
self.disconnect()
|
||||
Logger.services.info("\(Date()) Could not discover services")
|
||||
}))
|
||||
}
|
||||
|
||||
// Discover Characteristics of a Services
|
||||
func discoverCharacteristics(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Discovering characteristics for Meshtastic OTA service")
|
||||
guard let meshtasticOTAService = peripheral.meshtasticOTAService else {
|
||||
self.disconnect()
|
||||
return
|
||||
}
|
||||
peripheral.discoverCharacteristics([statusCharacteristicId], for: meshtasticOTAService)
|
||||
state = .discoveringCharacteristics(peripheral,
|
||||
Countdown(seconds: 10,
|
||||
closure: {
|
||||
self.disconnect()
|
||||
Logger.services.info("\(Date()) Could not discover characteristics")
|
||||
}))
|
||||
}
|
||||
|
||||
func setConnected(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Max write value with response: \(peripheral.maximumWriteValueLength(for: .withResponse))")
|
||||
Logger.services.info("Max write value without response: \(peripheral.maximumWriteValueLength(for: .withoutResponse))")
|
||||
guard let statusCharacteristic = peripheral.statusCharacteristic
|
||||
else {
|
||||
Logger.services.info("Missing status characteristic")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
peripheral.setNotifyValue(true, for: statusCharacteristic)
|
||||
state = .connected(peripheral)
|
||||
connected = true
|
||||
name = String(peripheral.name ?? "unknown")
|
||||
|
||||
checkStartTransfer()
|
||||
}
|
||||
|
||||
// Peripheral callback when its ready to receive more data without response
|
||||
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
|
||||
if transferOngoing && packageCounter < chunkCount {
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFile() {
|
||||
Logger.services.info("Start sending .bin file to device")
|
||||
|
||||
// 1. Get the data from the file(name) and copy data to dataBUffer
|
||||
guard let fileUrl, let data: Data = try? Data(contentsOf: fileUrl) else {
|
||||
Logger.services.info("Failed to open .bin file")
|
||||
return
|
||||
}
|
||||
dataBuffer = data
|
||||
dataLength = dataBuffer.count
|
||||
|
||||
// 1. Get the peripheral and its transfer characteristic
|
||||
guard let discoveredPeripheral = state.peripheral else { return }
|
||||
// Send dataLength to the device
|
||||
let sizeMessage = "OTA_SIZE:\(dataLength)"
|
||||
if let sizeData = sizeMessage.data(using: .utf8) {
|
||||
// Send sizeData to the peripheral
|
||||
// Assuming writeCharacteristic is the characteristic for sending messages
|
||||
discoveredPeripheral.writeValue(sizeData, for: otaCharacteristic!, type: .withoutResponse)
|
||||
Logger.services.info("Sent OTA size message: \(sizeMessage)")
|
||||
} else {
|
||||
Logger.services.info("Failed to encode OTA size message")
|
||||
return
|
||||
}
|
||||
|
||||
// Logger.services.info the total size of the data in hexadecimal format
|
||||
Logger.services.info("Total data size (hexadecimal): \(String(format: "%02X", self.dataBuffer.count))")
|
||||
transferOngoing = true
|
||||
|
||||
packageCounter = 0
|
||||
// Send the first chunk
|
||||
elapsedTime = 0.0
|
||||
sentBytes = 0
|
||||
firstAcknowledgeFromESP32 = false
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
|
||||
func writeDataToPeripheral(characteristic: CBCharacteristic) {
|
||||
// 1. Get the peripheral and its transfer characteristic
|
||||
guard let discoveredPeripheral = state.peripheral else { return }
|
||||
// ATT MTU - 3 bytes
|
||||
let maxWriteValueLength = discoveredPeripheral.maximumWriteValueLength(for: .withoutResponse)
|
||||
chunkSize = maxWriteValueLength - 3
|
||||
Logger.services.info("Chunk size: \(self.chunkSize), 0x\(String(format: "%02X", self.chunkSize))")
|
||||
// Get the data range
|
||||
var range: Range<Data.Index>
|
||||
|
||||
// 2. Loop through and send each chunk to the BLE device
|
||||
// check to see if the number of iterations completed and the peripheral can accept more data
|
||||
// package counter allows only "chunkCount" of data to be sent per time.
|
||||
while transferOngoing && packageCounter < chunkCount {
|
||||
// 3. Create a range based on the length of data to return
|
||||
range = (0..<min(chunkSize, dataBuffer.count))
|
||||
// 4. Get a subcopy copy of data
|
||||
let subData = dataBuffer.subdata(in: range)
|
||||
// Logger.services.info the first byte of the subData package as hexadecimal
|
||||
if let firstByte = subData.first {
|
||||
Logger.services.info("First byte of subData package: \(String(format: "%02X", firstByte))")
|
||||
}
|
||||
// 5. Send data chunk to BLE peripheral, send EOF when buffer is empty.
|
||||
if !dataBuffer.isEmpty {
|
||||
discoveredPeripheral.writeValue(subData, for: characteristic, type: .withoutResponse)
|
||||
packageCounter += 1
|
||||
// Logger.services.info(" Packages: \(packageCounter) bytes: \(subData.count)")
|
||||
} else {
|
||||
transferOngoing = false
|
||||
}
|
||||
|
||||
if discoveredPeripheral.canSendWriteWithoutResponse {
|
||||
Logger.services.info("BLE peripheral ready?: \(discoveredPeripheral.canSendWriteWithoutResponse)")
|
||||
}
|
||||
|
||||
// 6. Remove already sent data from buffer
|
||||
dataBuffer.removeSubrange(range)
|
||||
|
||||
// 7. calculate and Logger.services.info the transfer progress in %
|
||||
transferProgress = (1 - (Double(dataBuffer.count) / Double(dataLength))) * 100
|
||||
Logger.services.info("File transfer progress: \(String(format: "%.02f", self.transferProgress))%")
|
||||
sentBytes += chunkSize
|
||||
elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
|
||||
let kbPs = Double(sentBytes) / elapsedTime
|
||||
kBPerSecond = kbPs / 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBPeripheral {
|
||||
// Helper to find the service we're interested in.
|
||||
var meshtasticOTAService: CBService? {
|
||||
guard let services = services else { return nil }
|
||||
return services.first { $0.uuid == meshtasticOTAServiceId }
|
||||
}
|
||||
// Helper to find the characteristic we're interested in.
|
||||
var statusCharacteristic: CBCharacteristic? {
|
||||
guard let characteristics = meshtasticOTAService?.characteristics else {
|
||||
return nil
|
||||
}
|
||||
return characteristics.first { $0.uuid == statusCharacteristicId }
|
||||
}
|
||||
}
|
||||
|
||||
class Countdown {
|
||||
let timer: Timer
|
||||
init(seconds: TimeInterval, closure: @escaping () -> Void) {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in closure() })
|
||||
}
|
||||
deinit {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// ESP32BLEOTAViewModel2.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
import UIKit
|
||||
|
||||
private let meshtasticOTAServiceId = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B")
|
||||
private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") // ESP32 pTxCharacteristic ESP send (notifying)
|
||||
private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005") // ESP32 pOtaCharacteristic ESP write
|
||||
|
||||
|
||||
@MainActor
|
||||
final class ESP32BLEOTAViewModel2: ObservableObject {
|
||||
@Published var name = ""
|
||||
@Published var transferProgress: Double = 0
|
||||
@Published var otaStatus: LocalOTAStatusCode = .idle
|
||||
@Published var statusMessage: String = ""
|
||||
|
||||
private let ble = AsyncCentral()
|
||||
|
||||
func startOTA(binURL: URL) {
|
||||
Task {
|
||||
do {
|
||||
try await ble.waitUntilPoweredOn()
|
||||
let peripheral = try await ble.scan(for: meshtasticOTAServiceId)
|
||||
name = peripheral.name ?? "unknown"
|
||||
try await ble.connect(peripheral)
|
||||
otaStatus = .connected
|
||||
|
||||
let services = try await ble.discoverServices([meshtasticOTAServiceId], on: peripheral)
|
||||
guard let service = services.first(where: { $0.uuid == meshtasticOTAServiceId }) else { throw BLEError.serviceMissing }
|
||||
|
||||
let chars = try await ble.discoverCharacteristics([statusCharacteristicId, otaCharacteristicId],
|
||||
in: service,
|
||||
on: peripheral)
|
||||
guard
|
||||
let statusChar = chars.first(where: { $0.uuid == statusCharacteristicId }),
|
||||
let otaChar = chars.first(where: { $0.uuid == otaCharacteristicId })
|
||||
else { throw BLEError.characteristicMissing }
|
||||
|
||||
try await ble.setNotify(true, for: statusChar, on: peripheral)
|
||||
|
||||
// Setup the ackStream before we send the invitation
|
||||
let ackStream = ble.notifications(for: statusChar)
|
||||
|
||||
// Disable the idle timer till we're done
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
|
||||
// Start transfer
|
||||
let data = try Data(contentsOf: binURL)
|
||||
let sizeMsg = "OTA_SIZE:\(data.count)"
|
||||
try await ble.writeValue(Data(sizeMsg.utf8), for: otaChar, type: .withoutResponse, on: peripheral)
|
||||
|
||||
var buffer = data
|
||||
|
||||
for await notifyData in ackStream {
|
||||
if let value = notifyData.first, let code = DeviceBLEOTAStatusCode(rawValue: value) {
|
||||
switch code {
|
||||
case .WAITING_FOR_SIZE:
|
||||
// This probably doesn't happen because we already sent an OTA_SIZE meesage above
|
||||
Logger.services.info("[ESP BLE OTA] Device is waiting for size")
|
||||
self.statusMessage = "About to start..."
|
||||
|
||||
case .ERASING_FLASH:
|
||||
self.otaStatus = .preparing
|
||||
Logger.services.info("[ESP BLE OTA] Device is erasing the flash")
|
||||
self.statusMessage = "Preparing flash partition..."
|
||||
|
||||
case .READY_FOR_CHUNK, .CHUNK_ACK:
|
||||
self.otaStatus = .transferring
|
||||
self.statusMessage = "Transfer in progress..."
|
||||
|
||||
let chunk = buffer.prefix(peripheral.maximumWriteValueLength(for: .withoutResponse) - 3)
|
||||
guard !chunk.isEmpty else { break }
|
||||
try await ble.writeValue(chunk, for: otaChar, type: .withoutResponse, on: peripheral)
|
||||
buffer.removeFirst(chunk.count)
|
||||
transferProgress = 100 * (1 - Double(buffer.count) / Double(data.count))
|
||||
|
||||
case .OTA_COMPLETE:
|
||||
self.otaStatus = .completed
|
||||
self.statusMessage = "OTA Complete!"
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
|
||||
case .ERROR:
|
||||
self.otaStatus = .error
|
||||
self.statusMessage = "Device Reported an Error!"
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// handle error, update UI
|
||||
self.otaStatus = .error
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
Logger.services.error("OTA failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
//
|
||||
// ESP32DFUSheet.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Jake Bordens on 12/12/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import Network
|
||||
|
||||
struct ESP32OTAIntroSheet: View {
|
||||
private enum Step {
|
||||
case intro
|
||||
case updater
|
||||
}
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
let binFileURL: URL
|
||||
|
||||
|
||||
@State var showWifiUpdater = false
|
||||
@State var showBLEUpdater = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
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")
|
||||
Image(systemName: "arrow.up.right")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
}.listRowBackground(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
} footer: {
|
||||
Color.clear.frame(height: 5)
|
||||
}
|
||||
|
||||
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: {
|
||||
Text("I Know What I'm Doing")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
.padding()
|
||||
.listRowBackground(Color(UIColor.tertiarySystemBackground))
|
||||
}
|
||||
case .ble:
|
||||
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: {
|
||||
Text("I Know What I'm Doing")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
.padding()
|
||||
.listRowBackground(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
}.sheet(isPresented: $showWifiUpdater) {
|
||||
ESP32WifiOTASheet(binFileURL: binFileURL)
|
||||
}.sheet(isPresented: $showBLEUpdater) {
|
||||
ESP32BLEOTASheet(binFileURL: binFileURL)
|
||||
}
|
||||
.navigationTitle("ESP32 Update")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { // Standard placement for "Done" or "Close"
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum SupportedOTAMode {
|
||||
case none
|
||||
case wifi
|
||||
case ble
|
||||
}
|
||||
|
||||
private var OTAMode: SupportedOTAMode {
|
||||
if let connection = accessoryManager.activeConnection?.connection {
|
||||
if connection is TCPConnection {
|
||||
return .wifi
|
||||
} else if connection is BLEConnection {
|
||||
return .ble
|
||||
}
|
||||
|
||||
}
|
||||
return .none
|
||||
}
|
||||
// func beginBLEProcessButton() -> some View {
|
||||
// Button {
|
||||
// let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context)
|
||||
// if let connectedNode, let user = connectedNode.user {
|
||||
// Task {
|
||||
// do {
|
||||
// if let host {
|
||||
// let device = accessoryManager.activeConnection?.device
|
||||
// try await accessoryManager.sendRebootOta(fromUser: user, toUser: user, rebootOtaSeconds: 2)
|
||||
// try await accessoryManager.disconnect()
|
||||
// await ota.startUpdate(host: host, firmwareUrl: self.binFileURL)
|
||||
// if let device {
|
||||
// try await Task.sleep(for: .seconds(3))
|
||||
// try await accessoryManager.connect(to: device, retries: 5)
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// Logger.mesh.error("Reboot Failed")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } label: {
|
||||
// Label("Reboot into BLE OTA Update Mode", systemImage: "square.and.arrow.down")
|
||||
// .frame(maxWidth: .infinity)
|
||||
// }.buttonStyle(.bordered)
|
||||
// .controlSize(.large)
|
||||
// .disabled(accessoryManager.activeDeviceNum == nil)
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// OTAEnums.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/22/25.
|
||||
//
|
||||
|
||||
enum DeviceBLEOTAStatusCode: UInt8 {
|
||||
case WAITING_FOR_SIZE = 0
|
||||
case ERASING_FLASH = 1
|
||||
case READY_FOR_CHUNK = 2
|
||||
case CHUNK_ACK = 3
|
||||
case OTA_COMPLETE = 4
|
||||
case ERROR = 5
|
||||
}
|
||||
|
||||
enum LocalOTAStatusCode: String, CustomStringConvertible {
|
||||
var description: String { return self.rawValue }
|
||||
case idle = "Ready"
|
||||
case waitingForConnection = "Waiting for Connection"
|
||||
case connected = "Connected"
|
||||
case preparing = "Preparing"
|
||||
case transferring = "Uploading"
|
||||
case completed = "Completed"
|
||||
case error = "Error"
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// ESP32WifiOTASheet.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by jake on 12/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
struct ESP32WifiOTASheet: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@StateObject var ota = ESP32WifiOTAViewModel()
|
||||
|
||||
// The stuff were updating, and the place we're updating it to
|
||||
let binFileURL: URL
|
||||
@State var host: String?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
VStack {
|
||||
Text("Please do not leave this screen until this process is complete.")
|
||||
.multilineTextAlignment(.center)
|
||||
}.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Firmware File").font(.caption).foregroundColor(.secondary)
|
||||
Text("\(self.binFileURL.lastPathComponent)").font(.caption)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text("Network Location").font(.caption).foregroundColor(.secondary)
|
||||
Text("\(host ?? "Unknown")").font(.caption)
|
||||
}
|
||||
} footer: {
|
||||
Text("Please be sure this is correct before proceeding.")
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
CircularProgressView(progress: ota.progress, isIndeterminate: (ota.otaState == .preparing), size: 225.0, subtitleText: ota.otaState.rawValue)
|
||||
.frame(minHeight: 250.0)
|
||||
Spacer()
|
||||
}.listRowBackground(Color.clear)
|
||||
VStack {
|
||||
switch ota.otaState {
|
||||
case .idle:
|
||||
beginWifiProcessButton()
|
||||
|
||||
case .error:
|
||||
Text("Error: \(ota.errorMessage, default: "Unknown")")
|
||||
|
||||
default:
|
||||
Text("\(ota.statusMessage, default: "")")
|
||||
}
|
||||
}.listRowBackground(Color.clear)
|
||||
}.listRowSeparator(.hidden)
|
||||
}.navigationTitle("ESP32 WiFi Updater")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { // Standard placement for "Done" or "Close"
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}.disabled(![.idle, .completed, .error].contains(ota.otaState))
|
||||
}
|
||||
}
|
||||
}.task {
|
||||
if let connection = accessoryManager.activeConnection?.connection as? TCPConnection {
|
||||
self.host = await connection.host.stringValue
|
||||
}
|
||||
}.interactiveDismissDisabled(true)
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func beginWifiProcessButton() -> some View {
|
||||
Button {
|
||||
let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context)
|
||||
if let connectedNode, let user = connectedNode.user {
|
||||
Task {
|
||||
do {
|
||||
if let host {
|
||||
let device = accessoryManager.activeConnection?.device
|
||||
try await accessoryManager.sendRebootOta(fromUser: user, toUser: user, rebootOtaSeconds: 1)
|
||||
try await accessoryManager.disconnect()
|
||||
await ota.startUpdate(host: host, firmwareUrl: self.binFileURL)
|
||||
if let device {
|
||||
try await Task.sleep(for: .seconds(3))
|
||||
try await accessoryManager.connect(to: device, retries: 5)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.error("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Reboot into Wifi OTA Update Mode", systemImage: "square.and.arrow.down")
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}.buttonStyle(.bordered)
|
||||
.controlSize(.large)
|
||||
.disabled(accessoryManager.activeDeviceNum == nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,25 +6,13 @@ import OSLog
|
|||
import os // Required for OSAllocatedUnfairLock
|
||||
|
||||
@MainActor
|
||||
class Esp32WifiOTAViewModel: ObservableObject {
|
||||
enum OTAState: String, CustomStringConvertible {
|
||||
var description: String { self.rawValue }
|
||||
|
||||
case idle = "Idle"
|
||||
case preparing = "Preparing"
|
||||
case handshaking = "Sending Handshake"
|
||||
case waitingForConnection = "Waiting for Connection"
|
||||
case uploading = "Uploading"
|
||||
case success = "Success"
|
||||
case error = "Error"
|
||||
}
|
||||
class ESP32WifiOTAViewModel: ObservableObject {
|
||||
|
||||
// MARK: - Published State
|
||||
@Published var statusMessage: String = "Idle"
|
||||
@Published var progress: Double = 0.0
|
||||
@Published var isUpdating: Bool = false
|
||||
@Published var errorMessage: String? = nil
|
||||
@Published var otaState: OTAState = .idle
|
||||
@Published var errorMessage: String?
|
||||
@Published var otaState: LocalOTAStatusCode = .idle
|
||||
|
||||
// MARK: - Constants
|
||||
private let espPort: NWEndpoint.Port = 3232
|
||||
|
|
@ -36,20 +24,19 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
|
||||
// MARK: - Public Interface
|
||||
|
||||
func startUpdate(host: NWEndpoint.Host, firmwareUrl: URL, password: String? = nil) async {
|
||||
guard !isUpdating else { return }
|
||||
func startUpdate(host: String, firmwareUrl: URL, password: String? = nil) async {
|
||||
guard self.otaState == .idle else { return }
|
||||
|
||||
self.isUpdating = true
|
||||
self.progress = 0.0
|
||||
self.errorMessage = nil
|
||||
self.statusMessage = "Preparing..."
|
||||
self.otaState = .preparing
|
||||
self.statusMessage = "Connecting..."
|
||||
self.otaState = .waitingForConnection
|
||||
|
||||
var listener: NWListener?
|
||||
|
||||
defer {
|
||||
listener?.cancel()
|
||||
self.isUpdating = false
|
||||
self.otaState = .idle
|
||||
}
|
||||
|
||||
do {
|
||||
|
|
@ -65,7 +52,7 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
Logger.services.info("[ESP OTA] Listening on port \(localPort)")
|
||||
|
||||
self.statusMessage = "Waiting for device. This can take a while..."
|
||||
self.otaState = .handshaking
|
||||
|
||||
Logger.services.info("[ESP OTA] Starting Handshake loop...")
|
||||
|
||||
try await performHandshake(host: host,
|
||||
|
|
@ -73,11 +60,11 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
data: firmwareData,
|
||||
password: password)
|
||||
|
||||
self.otaState = .waitingForConnection
|
||||
self.otaState = .connected
|
||||
for try await _ in transferStream { break }
|
||||
|
||||
self.statusMessage = "Success!"
|
||||
self.otaState = .success
|
||||
self.otaState = .completed
|
||||
Logger.services.info("[ESP OTA] Update Complete")
|
||||
|
||||
} catch {
|
||||
|
|
@ -99,11 +86,12 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
func getPayload() -> Data { return currentPayload }
|
||||
}
|
||||
|
||||
private func performHandshake(host: NWEndpoint.Host, localPort: UInt16, data: Data, password: String?) async throws {
|
||||
private func performHandshake(host: String, localPort: UInt16, data: Data, password: String?) async throws {
|
||||
let initialPayload = try generateInvitationPayload(localPort: localPort, data: data, password: password, authNonce: nil)
|
||||
let state = HandshakeState(initialPayload: initialPayload)
|
||||
|
||||
let connection = NWConnection(host: host, port: espPort, using: .udp)
|
||||
let nwHost = NWEndpoint.Host(host)
|
||||
let connection = NWConnection(host: nwHost, port: espPort, using: .udp)
|
||||
defer { connection.cancel() }
|
||||
|
||||
connection.start(queue: .global())
|
||||
|
|
@ -111,10 +99,11 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
|
||||
Logger.services.info("[ESP OTA] UDP Connection Ready. Starting broadcast/listen loop.")
|
||||
|
||||
var okReceived = false
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
// Task A: Broadcaster
|
||||
group.addTask {
|
||||
while !Task.isCancelled {
|
||||
while !Task.isCancelled, !okReceived {
|
||||
let payload = await state.getPayload()
|
||||
connection.send(content: payload, completion: .contentProcessed { _ in })
|
||||
Logger.services.debug("[ESP OTA] Sent invitation packet")
|
||||
|
|
@ -129,9 +118,11 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
|
||||
if response == "OK" {
|
||||
Logger.services.info("[ESP OTA] Handshake OK received!")
|
||||
okReceived = true
|
||||
return
|
||||
}
|
||||
|
||||
// THIS IS UNTESTED
|
||||
if response.hasPrefix("AUTH") {
|
||||
Logger.services.info("[ESP OTA] Auth challenge received: \(response)")
|
||||
let components = response.components(separatedBy: " ")
|
||||
|
|
@ -144,6 +135,14 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
await state.updatePayload(newPayload)
|
||||
}
|
||||
}
|
||||
|
||||
if response == "ERASE" {
|
||||
Logger.services.info("[ESP OTA] Device is erasing the flash partition.")
|
||||
Task { @MainActor in
|
||||
self.otaState = .preparing
|
||||
self.statusMessage = "Preparing flash partition..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +175,7 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
|
||||
nonisolated private func generateInvitationPayload(localPort: UInt16, data: Data, password: String?, authNonce: String?) throws -> Data {
|
||||
let fileMD5 = Insecure.MD5.hash(data: data).map { String(format: "%02hhx", $0) }.joined()
|
||||
Logger.services.info("Firmware MD5 is \(fileMD5)")
|
||||
let fileSize = data.count
|
||||
var message = "0 \(localPort) \(fileSize) \(fileMD5)"
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ class Esp32WifiOTAViewModel: ObservableObject {
|
|||
switch state {
|
||||
case .ready:
|
||||
Task { @MainActor in
|
||||
self.otaState = .uploading
|
||||
self.otaState = .transferring
|
||||
do {
|
||||
try await self.performChunkedTransfer(connection: connection, data: data)
|
||||
await MainActor.run {
|
||||
|
|
@ -329,7 +329,7 @@ private struct FirmwareRow: View {
|
|||
case .uf2:
|
||||
UF2MassStorageView(fileURL: firmwareFile.localUrl)
|
||||
case .bin:
|
||||
ESP32DFUSheet(binFileURL: firmwareFile.localUrl)
|
||||
ESP32OTAIntroSheet(binFileURL: firmwareFile.localUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue