diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d4d38083..1525e229 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -97,6 +97,10 @@ }, "shouldTranslate" : false }, + " Send Reboot into DFU" : { + "comment" : "A button that initiates", + "isCommentAutoGenerated" : true + }, ": %@" : { "localizations" : { "it" : { @@ -520,6 +524,16 @@ }, "shouldTranslate" : false }, + "%@ %lf" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$lf" + } + } + } + }, "%@ %lld" : { "localizations" : { "en" : { @@ -1452,6 +1466,26 @@ "• %@" : { "shouldTranslate" : false }, + "• Navigate all the way back to **Locations** in the file picker." : { + "comment" : "A step in the firmware export process, describing the user's action to navigate back to the \"Locations\" folder in the file picker.", + "isCommentAutoGenerated" : true + }, + "• Select your USB device and tap **Save**." : { + "comment" : "A step in the firmware export process, instructing the user to save the file to their USB.", + "isCommentAutoGenerated" : true + }, + "• Tap the **Save Firmware to USB** button below." : { + "comment" : "A step in the firmware update process, describing how to save the firmware to a USB device.", + "isCommentAutoGenerated" : true + }, + "• The filename will be a random string ending in `.uf2` to prevent iOS caching." : { + "comment" : "A note explaining that the firmware filename will be a random string with a `.uf2` extension to prevent iOS caching.", + "isCommentAutoGenerated" : true + }, + "• You may see an error saying the file could not be saved. This is normal, as the device disconnects immediately after updating." : { + "comment" : "A note explaining that a file could not be saved, which is normal as the device disconnects after updating.", + "isCommentAutoGenerated" : true + }, "< 1%" : { "localizations" : { "it" : { @@ -3440,6 +3474,9 @@ } } } + }, + "Alpha" : { + }, "Alt" : { "localizations" : { @@ -4210,6 +4247,9 @@ } } } + }, + "Architecture" : { + }, "Are you sure you want to delete this message?" : { "localizations" : { @@ -4349,6 +4389,10 @@ } } }, + "Attributes" : { + "comment" : "A section header that lists the attributes of a managed object.", + "isCommentAutoGenerated" : true + }, "Australia / New Zealand" : { "localizations" : { "it" : { @@ -5136,6 +5180,10 @@ } } }, + "Begin Update" : { + "comment" : "A button that initiates the process of flashing new firmware to a device.", + "isCommentAutoGenerated" : true + }, "Biking" : { "localizations" : { "de" : { @@ -5176,6 +5224,14 @@ } } }, + "BIN" : { + "comment" : "The file format for the Nordic Semiconductor nRF52832 series chips.", + "isCommentAutoGenerated" : true + }, + "Binary Data (%lld bytes)" : { + "comment" : "A text label displaying the size of a binary data field. The argument is the size of the binary data in bytes.", + "isCommentAutoGenerated" : true + }, "BLE" : { "localizations" : { "it" : { @@ -7414,6 +7470,10 @@ } } }, + "Check For Updates" : { + "comment" : "A button label that triggers a refresh of the firmware API data.", + "isCommentAutoGenerated" : true + }, "CHG" : { "localizations" : { "it" : { @@ -9261,8 +9321,12 @@ } } } + }, + "Current Firmware Version" : { + }, "Current Firmware Version: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -9303,6 +9367,7 @@ } }, "Current Firmware Version: %@, Latest Firmware Version: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -9410,6 +9475,10 @@ } } }, + "Database Browser" : { + "comment" : "The title of the main view, which lists all entities in the database.", + "isCommentAutoGenerated" : true + }, "Date" : { "localizations" : { "de" : { @@ -10120,6 +10189,10 @@ } } }, + "Details" : { + "comment" : "The title of the view that lists detailed information about a single database entity.", + "isCommentAutoGenerated" : true + }, "Details..." : { "localizations" : { "sr" : { @@ -11140,6 +11213,10 @@ } } }, + "DFU Firmware Update" : { + "comment" : "The title of the screen that appears when flashing new firmware to a device.", + "isCommentAutoGenerated" : true + }, "Dilution of precision (DOP) PDOP used by default" : { "localizations" : { "it" : { @@ -12189,8 +12266,16 @@ } } } + }, + "Download" : { + "comment" : "A button label that downloads firmware.", + "isCommentAutoGenerated" : true + }, + "Downloaded" : { + }, "Drag & Drop Firmware Update" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -12225,6 +12310,7 @@ } }, "Drag & Drop Firmware Update Documentation" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -12259,6 +12345,7 @@ } }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -13165,6 +13252,7 @@ } }, "Enter DFU Mode" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -13216,6 +13304,14 @@ } } }, + "Entities" : { + "comment" : "The header text for the \"Entities\" section in the Core Data Browser.", + "isCommentAutoGenerated" : true + }, + "Entity Name" : { + "comment" : "A label describing the name of the entity in the database.", + "isCommentAutoGenerated" : true + }, "environment" : { "localizations" : { "de" : { @@ -14268,6 +14364,10 @@ } } }, + "False" : { + "comment" : "A label for a boolean value of false.", + "isCommentAutoGenerated" : true + }, "Favorite" : { "localizations" : { "de" : { @@ -14768,6 +14868,14 @@ } } }, + "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.", + "isCommentAutoGenerated" : true + }, + "Firmware Releases" : { + "comment" : "A section header that lists available firmware releases.", + "isCommentAutoGenerated" : true + }, "Firmware update docs" : { "localizations" : { "it" : { @@ -15474,6 +15582,10 @@ } } }, + "For security reasons, iOS cannot write directly to external USB devices. You must save the file manually." : { + "comment" : "An explanatory text about the need to manually save the firmware file.", + "isCommentAutoGenerated" : true + }, "Forty Eight Hours" : { "localizations" : { "de" : { @@ -16075,6 +16187,7 @@ } }, "Get NRF DFU from the App Store" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -16125,6 +16238,7 @@ } }, "Get the latest stable firmware" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -17507,6 +17621,7 @@ } }, "How to update Firmware" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -17746,6 +17861,10 @@ } } }, + "I Know What I'm Doing" : { + "comment" : "A button that dismisses the update warning alert when pressed.", + "isCommentAutoGenerated" : true + }, "IAQ" : { "localizations" : { "it" : { @@ -17898,6 +18017,10 @@ } } }, + "If connected, use the button below to reboot into DFU. Otherwise, press your device's reset button twice rapidly." : { + "comment" : "A description of what to do if the device is not connected to the computer.", + "isCommentAutoGenerated" : true + }, "If DOP is set, use HDOP / VDOP values instead of PDOP" : { "localizations" : { "it" : { @@ -17967,6 +18090,7 @@ } }, "If it is hard to access your device's reset button enter DFU mode here." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -18254,6 +18378,10 @@ } } }, + "Important Notes" : { + "comment" : "A section header that explains important notes about the view.", + "isCommentAutoGenerated" : true + }, "In addition to Config, Keys and BLE bonds will be wiped" : { "localizations" : { "sr" : { @@ -18572,6 +18700,10 @@ } } }, + "Install" : { + "comment" : "A button label that says \"Install\".", + "isCommentAutoGenerated" : true + }, "Invalid file content. Please check the file format." : { "localizations" : { "de" : { @@ -18932,6 +19064,14 @@ }, "Last seen device: %@" : { + }, + "Last Updated: %@" : { + "comment" : "A footer displaying the last time the firmware list was updated.", + "isCommentAutoGenerated" : true + }, + "Last Updated: Never" : { + "comment" : "A label displayed below a list of firmware releases, indicating that the list has never been updated.", + "isCommentAutoGenerated" : true }, "Latitude" : { "localizations" : { @@ -19303,6 +19443,10 @@ } } }, + "Loading" : { + "comment" : "A label displayed while waiting for data to load.", + "isCommentAutoGenerated" : true + }, "Loading Logs. . ." : { "localizations" : { "it" : { @@ -21370,6 +21514,10 @@ } } }, + "Metadata" : { + "comment" : "A section title for the metadata section of the entity detail view.", + "isCommentAutoGenerated" : true + }, "Metric" : { "localizations" : { "it" : { @@ -22749,6 +22897,7 @@ } }, "Newer firmware is available" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -22822,6 +22971,10 @@ } } }, + "nil" : { + "comment" : "A placeholder text that indicates a relationship value is \"nil\".", + "isCommentAutoGenerated" : true + }, "NMEA Positions" : { "localizations" : { "de" : { @@ -23117,6 +23270,10 @@ } } }, + "No Entities Found" : { + "comment" : "A message displayed when there are no entities in the Core Data store.", + "isCommentAutoGenerated" : true + }, "No Environment Metrics" : { "localizations" : { "it" : { @@ -23166,6 +23323,9 @@ } } } + }, + "No firmware has been downloaded for this device." : { + }, "No Interface" : { "localizations" : { @@ -23402,6 +23562,10 @@ } } }, + "No records found for %@" : { + "comment" : "A view to show when a list is empty, with an appropriate message and icon. The argument is a title for the view, the second is a system image name for the icon, and the third is a", + "isCommentAutoGenerated" : true + }, "No Response" : { "localizations" : { "de" : { @@ -23954,6 +24118,10 @@ } } }, + "Nordic DFU Update" : { + "comment" : "The title of the view.", + "isCommentAutoGenerated" : true + }, "Not a valid route file" : { "localizations" : { "it" : { @@ -24046,6 +24214,10 @@ } } }, + "Not Now" : { + "comment" : "The label of a button that dismisses a view without taking any action.", + "isCommentAutoGenerated" : true + }, "Not Present" : { "localizations" : { "fr" : { @@ -24282,6 +24454,10 @@ } } }, + "Object ID" : { + "comment" : "A label for the \"Object ID\" field in the entity details view.", + "isCommentAutoGenerated" : true + }, "Ok" : { "localizations" : { "sr" : { @@ -25055,6 +25231,7 @@ } }, "OTA Updates are not supported on this NRF Device." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -25089,6 +25266,7 @@ } }, "OTA Updates are not supported on your platform." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -26109,6 +26287,13 @@ } } } + }, + "Place your device in DFU mode and connect it via USB." : { + "comment" : "A step in the firmware update process, instructing the user to connect their device in DFU mode.", + "isCommentAutoGenerated" : true + }, + "Platform IO" : { + }, "Please be advised that because the map report is not encrypted, your data may be stored and displayed permanently by third parties. Meshtastic does not assume responsibility for any such storage, display or disclosure of this data." : { "localizations" : { @@ -26131,6 +26316,9 @@ } } } + }, + "Please connect to a device to see firmware updates." : { + }, "Please connect to a radio to configure settings." : { "localizations" : { @@ -26172,6 +26360,10 @@ } } }, + "Please do not leave this screen until this process is complete." : { + "comment" : "A message displayed in the bottom-right corner of the screen, instructing the user to wait until the DFU process is complete.", + "isCommentAutoGenerated" : true + }, "Please set a region" : { "localizations" : { "de" : { @@ -28800,6 +28992,10 @@ } } }, + "Relationships" : { + "comment" : "A section header that lists relationships for an entity.", + "isCommentAutoGenerated" : true + }, "Relayed by %d %@" : { "localizations" : { "en" : { @@ -30986,6 +31182,10 @@ } } }, + "Save Firmware to USB" : { + "comment" : "A button that allows the user to save the U2F firmware to a USB drive.", + "isCommentAutoGenerated" : true + }, "Save User Config to %@?" : { "localizations" : { "de" : { @@ -34834,6 +35034,9 @@ } } } + }, + "Stable" : { + }, "Standard" : { "localizations" : { @@ -35047,6 +35250,14 @@ } } }, + "Step 1: Connect Device" : { + "comment" : "A step label for connecting a device to flash a firmware update.", + "isCommentAutoGenerated" : true + }, + "Step 2: Save the File" : { + "comment" : "A step in the firmware update process, describing how to save the firmware file.", + "isCommentAutoGenerated" : true + }, "Store & Forward" : { "localizations" : { "it" : { @@ -35381,6 +35592,10 @@ } } }, + "Table Empty" : { + "comment" : "A message indicating that a table is empty.", + "isCommentAutoGenerated" : true + }, "Taiwan" : { "localizations" : { "ja" : { @@ -36028,6 +36243,14 @@ } } }, + "Test Devices API Refresh" : { + "comment" : "A button that refreshes the list of devices from the API.", + "isCommentAutoGenerated" : true + }, + "Test Firmware API Refresh" : { + "comment" : "A button label that refreshes the list of available firmware versions from the API.", + "isCommentAutoGenerated" : true + }, "Text Message" : { "localizations" : { "de" : { @@ -38389,6 +38612,10 @@ } } }, + "True" : { + "comment" : "A label indicating a boolean value of true.", + "isCommentAutoGenerated" : true + }, "Try Again" : { "localizations" : { "de" : { @@ -38771,6 +38998,14 @@ } } }, + "UF2" : { + "comment" : "A label indicating that the firmware is in UF2 format.", + "isCommentAutoGenerated" : true + }, + "UF2 Firmware Update" : { + "comment" : "The title of the view.", + "isCommentAutoGenerated" : true + }, "Ukraine 433MHz" : { "localizations" : { "it" : { @@ -39247,6 +39482,10 @@ } } }, + "Unsupported Installation" : { + "comment" : "The title of an alert that appears when attempting to install firmware on a device that is not supported.", + "isCommentAutoGenerated" : true + }, "Up" : { "localizations" : { "de" : { @@ -39373,6 +39612,10 @@ } } }, + "Update Warning" : { + "comment" : "The title of an alert that warns the user about flashing new firmware.", + "isCommentAutoGenerated" : true + }, "Update Your Firmware" : { "localizations" : { "de" : { @@ -39499,6 +39742,10 @@ } } }, + "Updating now..." : { + "comment" : "A message displayed while the firmware list is being updated.", + "isCommentAutoGenerated" : true + }, "Uplink Enabled" : { "localizations" : { "it" : { @@ -41928,6 +42175,7 @@ } }, "You can also update your Meshtastic device over bluetooth using the Nordic DFU app." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -42060,6 +42308,7 @@ } }, "Your Firmware is up to date" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -42303,6 +42552,10 @@ } } } + }, + "ZIP" : { + "comment" : "A label indicating that a firmware file is in ZIP format.", + "isCommentAutoGenerated" : true } }, "version" : "1.1" diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 83759679..b80dae84 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -17,6 +17,12 @@ 10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F32E2047D600536CE6 /* DatadogTrace */; }; 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 */; }; + 2315D19D2EECB3DA00E0FAE7 /* UTI+UF2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D19C2EECB3D400E0FAE7 /* UTI+UF2.swift */; }; + 2315D1A02EECB44800E0FAE7 /* UF2MassStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D19F2EECB44800E0FAE7 /* UF2MassStorageView.swift */; }; + 2315D1A22EECD2CB00E0FAE7 /* APIStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D1A12EECD2BE00E0FAE7 /* APIStructs.swift */; }; + 2315D1A52EED94E800E0FAE7 /* FirmwareFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */; }; + 2315D1A82EEF2ED400E0FAE7 /* SwiftDraw in Frameworks */ = {isa = PBXBuildFile; productRef = 2315D1A72EEF2ED400E0FAE7 /* SwiftDraw */; }; 231A53782E69ADB900216B99 /* NodeFilterParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231A53772E69ADB900216B99 /* NodeFilterParameters.swift */; }; 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; }; 231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; }; @@ -53,12 +59,23 @@ 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 */; }; + 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 */; }; + 23AB0E662EE35E0200AFA09D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AB0E652EE35E0200AFA09D /* Image.swift */; }; 23AD54692E2A6EAA0046E9AB /* AccessoryManager+FromRadio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AD54682E2A6EAA0046E9AB /* AccessoryManager+FromRadio.swift */; }; 23AD546B2E2AA5A80046E9AB /* AccessoryManager+ToRadio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AD546A2E2AA5A80046E9AB /* AccessoryManager+ToRadio.swift */; }; 23AD546D2E2AE9630046E9AB /* AccessoryManager+MQTT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AD546C2E2AE9630046E9AB /* AccessoryManager+MQTT.swift */; }; + 23C2BD292EE87D0300F6A997 /* CoreDataBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BD282EE87D0300F6A997 /* CoreDataBrowser.swift */; }; + 23C2BD2B2EE8993800F6A997 /* DeviceHardwareImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BD2A2EE8993800F6A997 /* DeviceHardwareImage.swift */; }; + 23C2BE252EE9A8E100F6A997 /* SupportedHardwareBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C2BE242EE9A8E100F6A997 /* SupportedHardwareBadge.swift */; }; + 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 */; }; 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 */; }; 23E23F922E392C2B00919073 /* LogRecord+StringRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */; }; 23F061B32E7B056600A1E2EA /* Logger+DataDog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */; }; 23F488122E32980B002C776F /* AccessoryManager+Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F488112E32980B002C776F /* AccessoryManager+Position.swift */; }; @@ -147,7 +164,6 @@ DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; }; DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; }; DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD268D8D2BCC90E2008073AE /* RouteEnums.swift */; }; - DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD354FD82BD96A0B0061A25F /* IAQScale.swift */; }; DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; }; @@ -315,6 +331,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 236CA24D2EE3073100A47F96 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DDDE5A0829AF163F00490C6C /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -333,6 +358,11 @@ 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = ""; }; 231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEAuthorizationHelper.swift; sourceTree = ""; }; + 23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAPI.swift; sourceTree = ""; }; + 2315D19C2EECB3D400E0FAE7 /* UTI+UF2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UTI+UF2.swift"; sourceTree = ""; }; + 2315D19F2EECB44800E0FAE7 /* UF2MassStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UF2MassStorageView.swift; sourceTree = ""; }; + 2315D1A12EECD2BE00E0FAE7 /* APIStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIStructs.swift; sourceTree = ""; }; + 2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareFile.swift; sourceTree = ""; }; 231A53772E69ADB900216B99 /* NodeFilterParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParameters.swift; sourceTree = ""; }; 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = ""; }; 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = ""; }; @@ -369,12 +399,22 @@ 237AEB962E1FE627003B7CE3 /* BLETransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLETransport.swift; sourceTree = ""; }; 237AEB982E20098B003B7CE3 /* BLEConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEConnection.swift; sourceTree = ""; }; 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = ""; }; + 2388EC392EDF8A1400F6F982 /* DFUModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFUModel.swift; sourceTree = ""; }; 23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RXTXIndicatorView.swift; sourceTree = ""; }; + 23AB0E652EE35E0200AFA09D /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 23AD54682E2A6EAA0046E9AB /* AccessoryManager+FromRadio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+FromRadio.swift"; sourceTree = ""; }; 23AD546A2E2AA5A80046E9AB /* AccessoryManager+ToRadio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+ToRadio.swift"; sourceTree = ""; }; 23AD546C2E2AE9630046E9AB /* AccessoryManager+MQTT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+MQTT.swift"; sourceTree = ""; }; + 23C2BD282EE87D0300F6A997 /* CoreDataBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataBrowser.swift; sourceTree = ""; }; + 23C2BD2A2EE8993800F6A997 /* DeviceHardwareImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHardwareImage.swift; sourceTree = ""; }; + 23C2BE242EE9A8E100F6A997 /* SupportedHardwareBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedHardwareBadge.swift; sourceTree = ""; }; + 23C2BE262EE9F4BD00F6A997 /* DeviceHardwareEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHardwareEntity.swift; sourceTree = ""; }; + 23C2BE292EEAF96A00F6A997 /* FirmwareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareViewModel.swift; sourceTree = ""; }; + 23C2BE302EEB823900F6A997 /* NRFDFUSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFDFUSheet.swift; sourceTree = ""; }; + 23C2BE332EEC3F9600F6A997 /* ESP32DFUSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32DFUSheet.swift; sourceTree = ""; }; 23D316922E5618D2002FA4FB /* AsyncGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncGate.swift; sourceTree = ""; }; 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResettableTimer.swift; sourceTree = ""; }; + 23DC50BA2EE76D9C0023838A /* URL+fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+fetch.swift"; sourceTree = ""; }; 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LogRecord+StringRepresentation.swift"; sourceTree = ""; }; 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+DataDog.swift"; sourceTree = ""; }; 23F488112E32980B002C776F /* AccessoryManager+Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Position.swift"; sourceTree = ""; }; @@ -474,7 +514,6 @@ DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = ""; }; DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = ""; }; - DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD354FD82BD96A0B0061A25F /* IAQScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAQScale.swift; sourceTree = ""; }; DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = ""; }; @@ -667,7 +706,76 @@ DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 236CA24C2EE3071A00A47F96 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + crowpanel_2_8.svg, + crowpanel_3_5.svg, + crowpanel_5_0.svg, + crowpanel_7_0.svg, + diy.svg, + heltec_mesh_pocket.svg, + heltec_v4.svg, + "heltec-ht62-esp32c3-sx1262.svg", + "heltec-mesh-node-t114-case.svg", + "heltec-mesh-node-t114.svg", + "heltec-mesh-solar.svg", + "heltec-v3-case.svg", + "heltec-v3.svg", + "heltec-vision-master-e213.svg", + "heltec-vision-master-e290.svg", + "heltec-vision-master-t190.svg", + "heltec-wireless-paper.svg", + "heltec-wireless-tracker.svg", + "heltec-wsl-v3.svg", + image_manifest.json, + "lilygo-tlora-pager.svg", + m5_c6l.svg, + meteor_pro.svg, + muzi_r1_neo.svg, + "nano-g2-ultra.svg", + pico.svg, + promicro.svg, + rak_3312.svg, + rak_wismesh_tag.svg, + "rak-wismesh-tap-v2.svg", + "rak-wismeshtap.svg", + rak2560.svg, + rak4631_case.svg, + rak4631.svg, + rak11200.svg, + rak11310.svg, + rpipicow.svg, + seeed_solar.svg, + seeed_xiao_nrf52_kit.svg, + "seeed-sensecap-indicator.svg", + "seeed-xiao-s3.svg", + "station-g2.svg", + "t-deck.svg", + "t-echo.svg", + "t-watch-s3.svg", + "tbeam-s3-core.svg", + tbeam.svg, + tdeck_pro.svg, + techo_lite.svg, + thinknode_m1.svg, + thinknode_m2.svg, + "tlora-t3s3-epaper.svg", + "tlora-t3s3-v1.svg", + "tlora-v2-1-1_6.svg", + "tlora-v2-1-1_8.svg", + "tracker-t1000-e.svg", + wio_tracker_l1_case.svg, + wio_tracker_l1_eink.svg, + "wio-tracker-wm1110.svg", + ); + target = DDC2E15326CE248E0042C5E4 /* Meshtastic */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ + 236CA20D2EE3036A00A47F96 /* images */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (236CA24C2EE3071A00A47F96 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = images; sourceTree = ""; }; DD4C11E02E8099C3003F2F2E /* PreferenceKeys */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = PreferenceKeys; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -689,7 +797,9 @@ 10D109F22E2047D600536CE6 /* DatadogSessionReplay in Frameworks */, 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */, 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */, + 2315D1A82EEF2ED400E0FAE7 /* SwiftDraw in Frameworks */, DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */, + 2388EC382EDF88E900F6F982 /* NordicDFU in Frameworks */, 10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -707,6 +817,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 23148E2E2EE1CCB100F0DB2C /* API */ = { + isa = PBXGroup; + children = ( + 2315D1A12EECD2BE00E0FAE7 /* APIStructs.swift */, + 23DC51382EE76DA20023838A /* Helpers */, + 23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */, + ); + path = API; + sourceTree = ""; + }; + 2315D19E2EECB42D00E0FAE7 /* U2F Mass Storage */ = { + isa = PBXGroup; + children = ( + 2315D19F2EECB44800E0FAE7 /* UF2MassStorageView.swift */, + ); + path = "U2F Mass Storage"; + sourceTree = ""; + }; + 2315D1A32EED94D700E0FAE7 /* Firmware */ = { + isa = PBXGroup; + children = ( + 23C2BE292EEAF96A00F6A997 /* FirmwareViewModel.swift */, + 2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */, + ); + path = Firmware; + sourceTree = ""; + }; 231B3F1E2D0879BC0069A07D /* Metrics Visualization */ = { isa = PBXGroup; children = ( @@ -766,6 +903,42 @@ path = Accessory; sourceTree = ""; }; + 23C2BD272EE87CFD00F6A997 /* Debugging */ = { + isa = PBXGroup; + children = ( + 23C2BD282EE87D0300F6A997 /* CoreDataBrowser.swift */, + ); + path = Debugging; + sourceTree = ""; + }; + 23C2BE282EEAF91900F6A997 /* Firmware */ = { + isa = PBXGroup; + children = ( + DDD6EEAE29BC024700383354 /* Firmware.swift */, + 2315D19E2EECB42D00E0FAE7 /* U2F Mass Storage */, + 23C2BE322EEC3F7800F6A997 /* ESP32 DFU */, + 23C2BE2F2EEB821400F6A997 /* NRF DFU */, + ); + path = Firmware; + sourceTree = ""; + }; + 23C2BE2F2EEB821400F6A997 /* NRF DFU */ = { + isa = PBXGroup; + children = ( + 2388EC392EDF8A1400F6F982 /* DFUModel.swift */, + 23C2BE302EEB823900F6A997 /* NRFDFUSheet.swift */, + ); + path = "NRF DFU"; + sourceTree = ""; + }; + 23C2BE322EEC3F7800F6A997 /* ESP32 DFU */ = { + isa = PBXGroup; + children = ( + 23C2BE332EEC3F9600F6A997 /* ESP32DFUSheet.swift */, + ); + path = "ESP32 DFU"; + sourceTree = ""; + }; 23D9D9312E50DA0E005D1C18 /* Protocols */ = { isa = PBXGroup; children = ( @@ -839,6 +1012,15 @@ path = Helpers; sourceTree = ""; }; + 23DC51382EE76DA20023838A /* Helpers */ = { + isa = PBXGroup; + children = ( + 2315D19C2EECB3D400E0FAE7 /* UTI+UF2.swift */, + 23DC50BA2EE76D9C0023838A /* URL+fetch.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 251926882C3BAF2E00249DF5 /* Actions */ = { isa = PBXGroup; children = ( @@ -915,6 +1097,7 @@ DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */, + 23C2BE262EE9F4BD00F6A997 /* DeviceHardwareEntity.swift */, ); path = CoreData; sourceTree = ""; @@ -960,14 +1143,13 @@ DDD5BB0E2C285F92007E03CA /* Logs */, DD93800C2BA74CE3008BEC06 /* Channels */, DD61937A2863876A00E59241 /* Config */, + 23C2BE282EEAF91900F6A997 /* Firmware */, DD97E96728EFE9A00056DDA4 /* About.swift */, DDD5BB152C28B1E4007E03CA /* AppData.swift */, DDD5BB082C285DDC007E03CA /* AppLog.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, - DDD6EEAE29BC024700383354 /* Firmware.swift */, - DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */, DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */, DDAB580C2B0DAA9E00147258 /* Routes.swift */, DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */, @@ -1163,6 +1345,7 @@ isa = PBXGroup; children = ( 237AEB8D2E1FE120003B7CE3 /* Accessory */, + 23148E2E2EE1CCB100F0DB2C /* API */, BCB6137F2C6728E700485544 /* AppIntents */, DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, @@ -1196,6 +1379,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + 23C2BD272EE87CFD00F6A997 /* Debugging */, DD47E3D726F2F21A00029299 /* Connect */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD6D5A312CA1176A00ED3032 /* Layouts */, @@ -1211,6 +1395,7 @@ DDC2E18826CE24EE0042C5E4 /* Model */ = { isa = PBXGroup; children = ( + 2315D1A32EED94D700E0FAE7 /* Firmware */, 2344A2AC2D66978000170A77 /* CoreData */, 231B3F1E2D0879BC0069A07D /* Metrics Visualization */, DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */, @@ -1221,6 +1406,7 @@ DDC2E18926CE24F70042C5E4 /* Resources */ = { isa = PBXGroup; children = ( + 236CA20D2EE3036A00A47F96 /* images */, 2339EA972E6C65570032C234 /* AppIcon.icon */, 2339EA992E6C65DC0032C234 /* AppIconDebug.icon */, DD98EB282E7A42CC0016320A /* AppIcon_Chirpy.icon */, @@ -1274,6 +1460,8 @@ 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */, 23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */, + 23C2BD2A2EE8993800F6A997 /* DeviceHardwareImage.swift */, + 23C2BE242EE9A8E100F6A997 /* SupportedHardwareBadge.swift */, ); path = Helpers; sourceTree = ""; @@ -1377,6 +1565,7 @@ DDD5BB0C2C285F00007E03CA /* Logger.swift */, 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */, DD6F65732C6CB80A0053C113 /* View.swift */, + 23AB0E652EE35E0200AFA09D /* Image.swift */, ); path = Extensions; sourceTree = ""; @@ -1430,6 +1619,8 @@ isa = PBXNativeTarget; buildConfigurationList = DDC2E17E26CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "Meshtastic" */; buildPhases = ( + 23148E312EE1F43B00F0DB2C /* Download Node SVG Images */, + 236CA24D2EE3073100A47F96 /* CopyFiles */, BB450974275599CE00509624 /* ShellScript */, DDC2E15026CE248E0042C5E4 /* Sources */, DDC2E15126CE248E0042C5E4 /* Frameworks */, @@ -1442,6 +1633,7 @@ DDDE5A0229AF163E00490C6C /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( + 236CA20D2EE3036A00A47F96 /* images */, DD4C11E02E8099C3003F2F2E /* PreferenceKeys */, ); name = Meshtastic; @@ -1454,6 +1646,8 @@ 102B5EB02E172F41003D191E /* DatadogRUM */, 10D109F12E2047D600536CE6 /* DatadogSessionReplay */, 10D109F32E2047D600536CE6 /* DatadogTrace */, + 2388EC372EDF88E900F6F982 /* NordicDFU */, + 2315D1A72EEF2ED400E0FAE7 /* SwiftDraw */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */; @@ -1525,6 +1719,8 @@ 25A978B82C13F8ED0003AAE7 /* XCLocalSwiftPackageReference "MeshtasticProtobufs" */, 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */, 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */, + 2388EC362EDF450200F6F982 /* XCRemoteSwiftPackageReference "IOS-DFU-Library" */, + 2315D1A62EEF2ED400E0FAE7 /* XCRemoteSwiftPackageReference "SwiftDraw" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; @@ -1575,6 +1771,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 23148E312EE1F43B00F0DB2C /* Download Node SVG Images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Download Node SVG Images"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "MARKER=\"${TARGET_TEMP_DIR}/first-build.marker\"\n\nif [ ! -f \"$MARKER\" ]; then\n # Treating as CLEAN build (or first build in this DerivedData)\n # Force update all the note board images\n \"$SRCROOT/scripts/download_images.py\" --output-dir \"$SRCROOT/Meshtastic/Resources/images\" --output-json \"$SRCROOT/Meshtastic/Resources/DeviceHardware.json\" --force\n touch \"$MARKER\"\nelse\n # Incremental build\n # update only if there was a change in the devices json\n \"$SRCROOT/scripts/download_images.py\" --output-dir \"$SRCROOT/Meshtastic/Resources/images\" --output-json \"$SRCROOT/Meshtastic/Resources/DeviceHardware.json\"\nfi\n"; + showEnvVarsInLog = 0; + }; BB450974275599CE00509624 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -1610,6 +1826,7 @@ files = ( 230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */, 25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */, + 23C2BE252EE9A8E100F6A997 /* SupportedHardwareBadge.swift in Sources */, 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, @@ -1625,9 +1842,11 @@ DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */, + 2315D19D2EECB3DA00E0FAE7 /* UTI+UF2.swift in Sources */, DDD5BB182C2F9C36007E03CA /* OSLogEntryLog.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */, + 23C2BE2A2EEAF96A00F6A997 /* FirmwareViewModel.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, @@ -1660,9 +1879,11 @@ DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */, DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, 2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */, + 23C2BD2B2EE8993800F6A997 /* DeviceHardwareImage.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, + 23C2BE272EE9F4BD00F6A997 /* DeviceHardwareEntity.swift in Sources */, D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, @@ -1693,6 +1914,7 @@ DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, 233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, + 2315D1A52EED94E800E0FAE7 /* FirmwareFile.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */, 23F061B32E7B056600A1E2EA /* Logger+DataDog.swift in Sources */, @@ -1701,6 +1923,7 @@ DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, DD9C700D2E8D5A9500106227 /* ChannelMessageRow.swift in Sources */, + 2315D1A02EECB44800E0FAE7 /* UF2MassStorageView.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */, DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */, @@ -1712,6 +1935,8 @@ DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */, + 23148E302EE1CCE500F0DB2C /* MeshtasticAPI.swift in Sources */, + 23C2BD292EE87D0300F6A997 /* CoreDataBrowser.swift in Sources */, 233E99B62D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift in Sources */, 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */, 2346A7192E2FB9A300CB9239 /* SerialConnection.swift in Sources */, @@ -1721,7 +1946,6 @@ DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */, DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, - DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, 23D9D9392E50DA97005D1C18 /* ResettableTimer.swift in Sources */, DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, @@ -1756,7 +1980,9 @@ DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */, DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */, + 23AB0E662EE35E0200AFA09D /* Image.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, + 23DC50BB2EE76D9C0023838A /* URL+fetch.swift in Sources */, 233E99CB2D85AAA900CC3A77 /* RainfallCompactWidget.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD9C70112E916EBD00106227 /* UpdateIntervalPicker.swift in Sources */, @@ -1803,6 +2029,7 @@ DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, 251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, + 2388EC3A2EDF8A1400F6F982 /* DFUModel.swift in Sources */, DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, @@ -1813,6 +2040,7 @@ DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, 233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, + 23C2BE312EEB823900F6A997 /* NRFDFUSheet.swift in Sources */, DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, 23D316932E5618D2002FA4FB /* AsyncGate.swift in Sources */, @@ -1857,12 +2085,14 @@ DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, + 23C2BE342EEC3F9600F6A997 /* ESP32DFUSheet.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, 233E99BC2D849C8C00CC3A77 /* WindCompactWidget.swift in Sources */, B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */, 232ED4C32E2C5E89009DA392 /* TCPTransport.swift in Sources */, BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */, + 2315D1A22EECD2CB00E0FAE7 /* APIStructs.swift in Sources */, D93068D72B8146690066FBC8 /* MessageText.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, ); @@ -2268,6 +2498,22 @@ minimumVersion = 3.3.0; }; }; + 2315D1A62EEF2ED400E0FAE7 /* XCRemoteSwiftPackageReference "SwiftDraw" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/swhitty/SwiftDraw"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.25.3; + }; + }; + 2388EC362EDF450200F6F982 /* XCRemoteSwiftPackageReference "IOS-DFU-Library" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/NordicSemiconductor/IOS-DFU-Library"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.16.0; + }; + }; 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -2317,6 +2563,16 @@ package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; productName = DatadogTrace; }; + 2315D1A72EEF2ED400E0FAE7 /* SwiftDraw */ = { + isa = XCSwiftPackageProductDependency; + package = 2315D1A62EEF2ED400E0FAE7 /* XCRemoteSwiftPackageReference "SwiftDraw" */; + productName = SwiftDraw; + }; + 2388EC372EDF88E900F6F982 /* NordicDFU */ = { + isa = XCSwiftPackageProductDependency; + package = 2388EC362EDF450200F6F982 /* XCRemoteSwiftPackageReference "IOS-DFU-Library" */; + productName = NordicDFU; + }; 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */ = { isa = XCSwiftPackageProductDependency; productName = MeshtasticProtobufs; diff --git a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3b339cec..06b682e0 100644 --- a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2569905853aec088d5bac6b540eac77f78963f88b406e8dd95a88c40623cc8b4", + "originHash" : "943f1047f8d99b0600f1c91f14d7bf4808ab1caf172ae4d7f3ebea325c27437f", "pins" : [ { "identity" : "cocoamqtt", @@ -15,8 +15,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DataDog/dd-sdk-ios.git", "state" : { - "revision" : "d0a42d8067665cb6ee86af51251ccc071f62bd54", - "version" : "2.29.0" + "revision" : "8d67e973ff4a958cb536263cb816646ee904c508", + "version" : "3.3.0" + } + }, + { + "identity" : "ios-dfu-library", + "kind" : "remoteSourceControl", + "location" : "https://github.com/NordicSemiconductor/IOS-DFU-Library", + "state" : { + "revision" : "4773d7eed944684dfdd177a4a91af8a89ebbaac8", + "version" : "4.16.0" } }, { @@ -63,6 +72,24 @@ "revision" : "102a647b573f60f73afdce5613a51d71349fe507", "version" : "1.30.0" } + }, + { + "identity" : "swiftdraw", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swhitty/SwiftDraw", + "state" : { + "revision" : "17d55c17540f3eb10685058e803d7ae73d9bf9d3", + "version" : "0.25.3" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } } ], "version" : 3 diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 53375016..b2d34393 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4", + "originHash" : "33a490b0eed23f6be325b80c313e6b146614a761edd63ec6dc35cb21f5df06b9", "pins" : [ + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "state" : { + "revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114", + "version" : "3.9.0" + } + }, { "identity" : "cocoamqtt", "kind" : "remoteSourceControl", @@ -19,6 +28,15 @@ "version" : "3.3.0" } }, + { + "identity" : "ios-dfu-library", + "kind" : "remoteSourceControl", + "location" : "https://github.com/NordicSemiconductor/IOS-DFU-Library", + "state" : { + "revision" : "4773d7eed944684dfdd177a4a91af8a89ebbaac8", + "version" : "4.16.0" + } + }, { "identity" : "mqttcocoaasyncsocket", "kind" : "remoteSourceControl", @@ -55,6 +73,24 @@ "version" : "4.0.8" } }, + { + "identity" : "svgkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SVGKit/SVGKit", + "state" : { + "revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666", + "version" : "3.0.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "b1fa4ef41fe21b13120c034854042d12c43f66b2", + "version" : "1.7.1" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", @@ -63,6 +99,15 @@ "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", "version" : "1.29.0" } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } } ], "version" : 3 diff --git a/Meshtastic/API/APIStructs.swift b/Meshtastic/API/APIStructs.swift new file mode 100644 index 00000000..0147a595 --- /dev/null +++ b/Meshtastic/API/APIStructs.swift @@ -0,0 +1,61 @@ +// +// APIStructs.swift +// Meshtastic +// +// Created by jake on 12/12/25. +// + +/// Device Hardware API +struct DeviceHardware: Codable { + let hwModel: Int + let hwModelSlug: String + let platformioTarget: String + let architecture: Architecture + let activelySupported: Bool + let displayName: String + let supportLevel: Int? + let tags: [String]? + let images: [String]? + let requiresDfu: Bool? + let hasInkHud: Bool? + let partitionScheme: String? + let hasMui: Bool? +} +enum Architecture: String, Codable, Identifiable { + case esp32 = "esp32" + case esp32C3 = "esp32-c3" + case esp32S3 = "esp32-s3" + case nrf52840 = "nrf52840" + case rp2040 = "rp2040" + case esp32C6 = "esp32-c6" + + var id: String { rawValue } +} + +/// Firmware Release Lists +struct FirmwareReleases: Codable { + let releases: Releases + let pullRequests: [FirmwareRelease] +} +struct Releases: Codable { + let stable, alpha: [FirmwareRelease] +} +struct FirmwareRelease: Codable { + let id, title: String + let pageURL: String + let zipURL: String + let releaseNotes: String + + enum CodingKeys: String, CodingKey { + case id, title + case pageURL = "page_url" + case zipURL = "zip_url" + case releaseNotes = "release_notes" + } + + enum ReleaseType: String { + case stable = "Stable" + case alpha = "Alpha" + case unlisted = "Unlisted" + } +} diff --git a/Meshtastic/API/FirmwareReleaseEntity.swift b/Meshtastic/API/FirmwareReleaseEntity.swift new file mode 100644 index 00000000..e69de29b diff --git a/Meshtastic/API/Helpers/URL+fetch.swift b/Meshtastic/API/Helpers/URL+fetch.swift new file mode 100644 index 00000000..820fef67 --- /dev/null +++ b/Meshtastic/API/Helpers/URL+fetch.swift @@ -0,0 +1,99 @@ +// +// URL+fetch.swift +// Meshtastic +// +// Created by jake on 12/6/25. +// + +import Foundation + +extension URL { + + /// Custom error type for the URL extension + enum TimeoutError: Error, LocalizedError { + case timedOut(TimeInterval) + + var errorDescription: String? { + switch self { + case .timedOut(let seconds): + return "The operation timed out after \(seconds) seconds." + } + } + } + + /// Fetches data from the URL (local or remote) with a strict timeout. + /// - Parameter timeout: The duration in seconds to wait before throwing an error. + /// - Returns: The `Data` retrieved. + func data(timeout: TimeInterval) async throws -> Data { + + return try await withThrowingTaskGroup(of: Data.self) { group in + + // Task 1: The Fetch Operation + group.addTask { + if self.isFileURL { + // Handle Local Files + // Note: Data(contentsOf:) is synchronous (blocking). + // Running it inside a Task allows it to be raced, though + // the underlying thread may remain blocked until OS IO completes + // if cancellation occurs. + return try Data(contentsOf: self) + } else { + // Handle Remote Network Requests + let (data, _) = try await URLSession.shared.data(from: self) + return data + } + } + + // Task 2: The Timer + group.addTask { + // Convert seconds to nanoseconds + let nanoseconds = UInt64(timeout * 1_000_000_000) + try await Task.sleep(nanoseconds: nanoseconds) + + // If we wake up, it means the fetch hasn't finished yet + throw TimeoutError.timedOut(timeout) + } + + // Race Handling + + // Wait for the first task to finish (either success or error) + guard let result = try await group.next() else { + // Should not be reachable, but required by compiler + throw URLError(.unknown) + } + + // If we are here, one task finished successfully. + // Cancel the other task immediately. + group.cancelAll() + + return result + } + } + + /// Performs a HEAD request to fetch the ETag header for the URL. + /// - Parameter session: The URLSession to use (defaults to .shared). + /// - Returns: The ETag string if found and the request is successful, otherwise nil. + func eTag(using session: URLSession = .shared) async throws -> String? { + var request = URLRequest(url: self) + request.httpMethod = "HEAD" + + // Ensure we don't use the local cache so we get the real ETag from the server + request.cachePolicy = .reloadIgnoringLocalCacheData + + let (_, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + return nil + } + + // Optional: Check for success status codes (200-299) + guard (200...299).contains(httpResponse.statusCode) else { + // You might want to return nil or throw a specific error here + // depending on your requirements (e.g. 404 Not Found) + return nil + } + + // Header lookup is case-insensitive + return httpResponse.value(forHTTPHeaderField: "ETag") + } +} diff --git a/Meshtastic/API/Helpers/UTI+UF2.swift b/Meshtastic/API/Helpers/UTI+UF2.swift new file mode 100644 index 00000000..5df98258 --- /dev/null +++ b/Meshtastic/API/Helpers/UTI+UF2.swift @@ -0,0 +1,40 @@ +// +// UTI+UF2.swift +// Meshtastic +// +// Created by jake on 12/12/25. +// + +import UniformTypeIdentifiers +import SwiftUI + +extension UTType { + // Define a custom type for your firmware + // Identifier: Use your bundle ID prefix (e.g., com.yourcompany.firmware) + static let UF2Firmware = UTType(exportedAs: "com.meshtastic.uf2-firmware") +} + + +struct FirmwareDocument: FileDocument { + // 1. Tell the system this document supports your custom UTType + static var readableContentTypes: [UTType] { [.UF2Firmware] } + + var firmwareData: Data + + init(data: Data) { + self.firmwareData = data + } + + // Initialize from an existing file (Read) + init(configuration: ReadConfiguration) throws { + guard let data = configuration.file.regularFileContents else { + throw CocoaError(.fileReadCorruptFile) + } + self.firmwareData = data + } + + // Prepare data for saving (Write) + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + return FileWrapper(regularFileWithContents: firmwareData) + } +} diff --git a/Meshtastic/API/MeshtasticAPI.swift b/Meshtastic/API/MeshtasticAPI.swift new file mode 100644 index 00000000..a2d41171 --- /dev/null +++ b/Meshtastic/API/MeshtasticAPI.swift @@ -0,0 +1,395 @@ +// +// MeshtasticAPI.swift +// Meshtastic +// +// Created by Jake Bordens on 12/4/25. +// + +import Foundation +import OSLog +import SwiftUI +import CoreData + +extension MeshtasticAPI { + enum MeshtasticAPIError: Error, LocalizedError { + case timedOut(TimeInterval) + case unableToRetreviveJSON + case unableToFindOrCreateEntity + case unknownArchitecture + case unknownPlatformIOTarget + var errorDescription: String? { + switch self { + case .timedOut(let seconds): + return "The operation timed out after \(seconds) seconds." + case .unableToRetreviveJSON: + return "Unable to retreive device hardware information." + case .unableToFindOrCreateEntity: + return "Unable to find or create Core Data entity." + case .unknownArchitecture: + return "Unknown architecture." + case .unknownPlatformIOTarget: + return "Unknown platformio target." + } + } + } +} + +class MeshtasticAPI: ObservableObject, @unchecked Sendable { + // Singleton Access + static let shared = { + MeshtasticAPI(container: PersistenceController.shared.container) + }() + + // MARK: - Constants + static let deviceURLEndpoint = URL(string: "https://api.meshtastic.org/resource/deviceHardware")! + static let imageURLPrefix = URL(string: "https://flasher.meshtastic.org/img/devices/")! + static let firmwareURLEndpoint = URL(string: "https://api.meshtastic.org/github/firmware/list")! + + // MARK: - Private properties + private let fileManager = FileManager.default + private let decoder = JSONDecoder() + private let container: NSPersistentContainer + + @Published var isLoadingDeviceList: Bool = false + @Published var isLoadingFirmwareList: Bool = false + + private init(container: NSPersistentContainer) { + self.container = container + Task.detached { + try? await self.refreshDevicesAPIData() + try? await self.refreshFirmwareAPIData() + } + } + + // MARK: - Main Logic + + func refreshFirmwareAPIData() async throws { + await MainActor.run { + self.isLoadingFirmwareList = true + } + + let apiData = try await Self.firmwareURLEndpoint.data(timeout: 5.0) + + let decodedFirmware = try decoder.decode(FirmwareReleases.self, from: apiData) + + let stableVersions = Set(decodedFirmware.releases.stable.map { $0.id }) + let alphaVersions = Set(decodedFirmware.releases.alpha.map { $0.id }) + + await withTaskGroup(of: Void.self) { group in + + for stableRelease in decodedFirmware.releases.stable { + group.addTask { + await self.processFirmware(release: stableRelease, releaseType: .stable) + } + } + + for alphaRelease in decodedFirmware.releases.alpha { + group.addTask { + await self.processFirmware(release: alphaRelease, releaseType: .alpha) + } + } + } + + // Anything that's left in stableVersions and alphaVersions is no longer present in the API and should be deleted. + let context = container.newBackgroundContext() + context.performAndWait { + let deleteRequest = FirmwareReleaseEntity.fetchRequest() + deleteRequest.predicate = Self.firmwareCompoundPredicate(stableVersions: stableVersions, alphaVersions: alphaVersions) + if let objectsToDelete = try? context.fetch(deleteRequest) { + for object in objectsToDelete { + Logger.services.info("Deleting orphaned firmware release: \(object.versionId ?? "unknown")") + context.delete(object) + } + } + } + + // Save the deletions if any + if context.hasChanges { + try? context.save() + } + + // Save the last update date for the firmware + UserDefaults.lastFirmwareAPIUpdate = Date() + + await MainActor.run { + self.isLoadingFirmwareList = false + } + + } + + func refreshDevicesAPIData() async throws { + await MainActor.run { + self.isLoadingDeviceList = true + } + + // PHASE 1: Network (Async) - Get the JSON first + var apiData: Data? + do { + apiData = try await Self.deviceURLEndpoint.data(timeout: 5.0) + } catch { + Logger.services.error("Unable to fetch device hardware from network: \(error.localizedDescription, privacy: .public)") + } + + // Fallback to local bundle + if apiData == nil { + if let bundledJsonURL = Bundle.main.url(forResource: "DeviceHardware.json", withExtension: nil) { + apiData = try? Data(contentsOf: bundledJsonURL) + } + } + + guard let finalData = apiData else { + throw MeshtasticAPIError.unableToRetreviveJSON + } + + // Decode Swift Structs (Safe to do off the DB thread) + let decodedDevices = try decoder.decode([DeviceHardware].self, from: finalData) + + // PHASE 2: Database (Sync) - Update Devices & Tags + let context = container.newBackgroundContext() + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + + // We will perform the bulk update and return a simple list of images we need to check next. + // We DO NOT do network calls for images inside this block. + try await context.perform { + + // 1. Update Devices and Tags + for device in decodedDevices { + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "platformioTarget == %@", device.platformioTarget) + fetchRequest.fetchLimit = 1 + + let existing = try? context.fetch(fetchRequest).first + let deviceEntity = existing ?? DeviceHardwareEntity(context: context) + + // Update Properties + deviceEntity.hwModel = Int64(device.hwModel) + deviceEntity.hwModelSlug = device.hwModelSlug + deviceEntity.platformioTarget = device.platformioTarget + deviceEntity.architecture = device.architecture.rawValue + deviceEntity.activelySupported = device.activelySupported + deviceEntity.displayName = device.displayName + + // Handle Tags (Helper function is now synchronous) + var tags = Set() + if let tagList = device.tags { + for tagString in tagList { + // Safe because findOrCreateTag is synchronous and uses `context` + if let tagEntity = try? Self.findOrCreateTag(tag: tagString, context: context) { + tags.insert(tagEntity) + } + } + } + deviceEntity.tags = tags as NSSet + } + + // 2. Cleanup Orphans + Self.deleteOrphanedTags(context: context) + + // 3. Save Device Metadata + if context.hasChanges { + try context.save() + } + } + + // PHASE 3: Images (Async Mixed) + // Now that the devices exist in DB, we process images one by one. + // We loop through the *Decoded Structs* (not DB objects) to get URLs. + await withTaskGroup(of: Void.self) { group in + for device in decodedDevices { + group.addTask { + guard let imagesList = device.images else { return } + for imageName in imagesList { + await self.processImage(imageName: imageName, platform: device.platformioTarget) + } + } + } + } + + // Final cleanup of images (Sync) + try await context.perform { + Self.deleteOrphanedImages(context: context) + if context.hasChanges { try context.save() } + } + + await MainActor.run { + self.isLoadingDeviceList = false + } + + } + + private func processFirmware(release: FirmwareRelease, releaseType: FirmwareRelease.ReleaseType) async { + let context = container.newBackgroundContext() + + await context.perform { + let fetchRequest = FirmwareReleaseEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "versionId == %@", release.id) + fetchRequest.fetchLimit = 1 + + let existingRelease = (try? context.fetch(fetchRequest).first) ?? FirmwareReleaseEntity(context: context) + existingRelease.versionId = release.id + existingRelease.title = release.title + existingRelease.releaseNotes = release.releaseNotes + existingRelease.pageUrl = release.pageURL + existingRelease.releaseType = releaseType.rawValue + + let cleanString = release.id.hasPrefix("v") ? release.id.dropFirst() : Substring(release.id) + let parts = cleanString.split(separator: ".") + if parts.count >= 3 { + existingRelease.versionMajor = Int32(parts[0]) ?? 0 + existingRelease.versionMinor = Int32(parts[1]) ?? 0 + existingRelease.versionPatch = Int32(parts[2]) ?? 0 + } + + try? context.save() + Logger.services.info("Saving firmware release \(release.id) in database.") + } + } + + /// Handles the logic of checking ETag -> Checking DB -> Downloading -> Bundle Fallback -> Saving + private func processImage(imageName: String, platform: String ) async { + let url = Self.imageURLPrefix.appendingPathComponent(imageName) + + // 1. Network: Try to get ETag (Optional - might fail if offline or timeout) + let remoteETag = try? await url.eTag() + + let context = container.newBackgroundContext() + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + + // 2. DB: Check if we already have this version or a usable cached version + let dbStatus: (isUpToDate: Bool, hasData: Bool) = await context.perform { + let request = DeviceHardwareImageEntity.fetchRequest() + request.predicate = NSPredicate(format: "fileName == %@", imageName) + request.fetchLimit = 1 + + if let existing = try? context.fetch(request).first, + let data = existing.svgData, !data.isEmpty { + + // A: If we have a remote tag, does it match? + if let rTag = remoteETag { + return (existing.eTag == rTag, true) + } + + // B: We are offline (no remote ETag), but we have data. Keep it. + return (true, true) + } + + // No data in DB + return (false, false) + } + + if dbStatus.isUpToDate { + Logger.services.debug("Image \(imageName) is up to date (or cached offline).") + return + } + + // 3. Acquire Data (Network Primary -> Bundle Secondary) + var dataToSave: Data? + var eTagToSave: String? + + // A: Attempt Network Download (only if we successfully got an ETag previously) + if let rTag = remoteETag { + if let networkData = try? await url.data(timeout: 5.0) { + dataToSave = networkData + eTagToSave = rTag + } + } + + // B: Fallback to Bundle if Network failed or returned no data + if dataToSave == nil { + Logger.services.debug("Network unavailable or failed for \(imageName). Checking local bundle.") + + // Look in the 'images' subdirectory + if let bundleURL = Bundle.main.url(forResource: imageName, withExtension: nil, subdirectory: "images"), + let bundleData = try? Data(contentsOf: bundleURL) { + + dataToSave = bundleData + // We use "bundled" as a placeholder ETag. + // Next time the app runs with internet, "bundled" != "real_etag", forcing an update. + eTagToSave = "bundled" + } + } + + // 4. DB: Save Image and Link to Device + guard let finalData = dataToSave, let finalETag = eTagToSave else { + Logger.services.error("Could not find image \(imageName) in Network or Bundle.") + return + } + + await context.perform { + // Find the Device (we must fetch it in THIS context) + let deviceReq = DeviceHardwareEntity.fetchRequest() + deviceReq.predicate = NSPredicate(format: "platformioTarget == %@", platform) + + guard let deviceEntity = try? context.fetch(deviceReq).first else { return } + + // Find or Create Image Entity + let imageReq = DeviceHardwareImageEntity.fetchRequest() + imageReq.predicate = NSPredicate(format: "fileName == %@", imageName) + + let existingImg = try? context.fetch(imageReq).first + let imageEntity = existingImg ?? DeviceHardwareImageEntity(context: context) + + imageEntity.fileName = imageName + imageEntity.eTag = finalETag + imageEntity.svgData = finalData + + // Create Relationship + deviceEntity.addToImages(imageEntity) + + try? context.save() + Logger.services.info("Saving \(imageName) in database. eTag=\(finalETag)") + } + } + + // MARK: - Helpers + + // Removed @MainActor - this must run on the background context passed in + private static func findOrCreateTag(tag: String, context: NSManagedObjectContext) throws -> DeviceHardwareTagEntity { + let fetchRequest = DeviceHardwareTagEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "tag == %@", tag) + fetchRequest.fetchLimit = 1 + + if let existingTag = try context.fetch(fetchRequest).first { + return existingTag + } + + let newTag = DeviceHardwareTagEntity(context: context) + newTag.tag = tag + return newTag + } + + private static func deleteOrphanedTags(context: NSManagedObjectContext) { + let req = DeviceHardwareTagEntity.fetchRequest() + req.predicate = NSPredicate(format: "devices.@count == 0") + if let tags = try? context.fetch(req) { + tags.forEach { context.delete($0) } + } + } + + private static func deleteOrphanedImages(context: NSManagedObjectContext) { + let req = DeviceHardwareImageEntity.fetchRequest() + req.predicate = NSPredicate(format: "device == nil") + if let images = try? context.fetch(req) { + images.forEach { context.delete($0) } + } + } + + // Helper to build compound predicate for firmware deletion (selects orphans) + static func firmwareCompoundPredicate(stableVersions: Set, alphaVersions: Set) -> NSPredicate { + let stablePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "releaseType == %@", FirmwareRelease.ReleaseType.stable.rawValue), + NSPredicate(format: "NOT (versionId IN %@)", stableVersions) + ]) + let alphaPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "releaseType == %@", FirmwareRelease.ReleaseType.alpha.rawValue), + NSPredicate(format: "NOT (versionId IN %@)", alphaVersions) + ]) + return NSCompoundPredicate(orPredicateWithSubpredicates: [stablePredicate, alphaPredicate]) + } +} + +// Image Manifest Decoding +private struct ImageManifest: Codable { + let files: [String: [String: String]] + let api_hash: String +} diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 7c928b3d..8016be4f 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -826,7 +826,7 @@ extension AccessoryManager { throw AccessoryError.ioFailed("removeNode: Unable to serialize admin packet") } - let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser?.longName ?? "#\(toUserNum)") by \(fromUser?.longName ?? "#\(fromUser)")" + let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser?.longName ?? "#\(toUserNum)") by \(fromUser?.longName ?? "#\(fromUserNum)")" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) return Int64(meshPacket.id) } diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index 12c9e33c..dab72b65 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -353,6 +353,9 @@ class BLETransport: Transport { if let shortName = fetchedMyInfo[0].user?.shortName { device.shortName = shortName } + if let version = fetchedMyInfo[0].user?.userNode?.metadata?.firmwareVersion { + device.firmwareVersion = version + } } } catch { // No-op diff --git a/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/Contents.json b/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/Contents.json deleted file mode 100644 index 1482d0f8..00000000 --- a/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "play_store_icon_114px-4.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png b/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json deleted file mode 100644 index 418dd7fe..00000000 --- a/Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-ht62-esp32c3-sx1262.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json deleted file mode 100644 index a4f550b7..00000000 --- a/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-mesh-node-t114-case.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json deleted file mode 100644 index 4234e370..00000000 --- a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec_mesh_pocket.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json deleted file mode 100644 index 42c0472b..00000000 --- a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-v3-case.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json deleted file mode 100644 index 687a7da9..00000000 --- a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-vision-master-e213.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json deleted file mode 100644 index 13ddda16..00000000 --- a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-vision-master-e290.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json deleted file mode 100644 index a1a7444e..00000000 --- a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-wireless-paper.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json deleted file mode 100644 index d13152fe..00000000 --- a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-wireless-tracker.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json deleted file mode 100644 index dea94fc1..00000000 --- a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "heltec-wsl-v3.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json deleted file mode 100644 index 1febc627..00000000 --- a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tbeam-s3-core.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png b/Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png deleted file mode 100644 index 7c5895c3..00000000 Binary files a/Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/NANOG1.imageset/Contents.json b/Meshtastic/Assets.xcassets/NANOG1.imageset/Contents.json deleted file mode 100644 index e8161263..00000000 --- a/Meshtastic/Assets.xcassets/NANOG1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json deleted file mode 100644 index fe8b1d15..00000000 --- a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "nano-g2-ultra.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json b/Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json deleted file mode 100644 index 0fbd5109..00000000 --- a/Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "promicro.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg b/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg deleted file mode 100644 index 8c5ce28e..00000000 --- a/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json b/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json deleted file mode 100644 index 60b17db3..00000000 --- a/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "rak4631_case.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json b/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json deleted file mode 100644 index 87088506..00000000 --- a/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "pico.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json deleted file mode 100644 index d1ac2a26..00000000 --- a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "seeed_solar.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json deleted file mode 100644 index 5f4c592b..00000000 --- a/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wio_tracker_l1_case.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json deleted file mode 100644 index fdd4019e..00000000 --- a/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "seeed-xiao-s3.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json b/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json deleted file mode 100644 index 3870939e..00000000 --- a/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "seeed-sensecap-indicator.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json deleted file mode 100644 index f8a70d36..00000000 --- a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "solar_node.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png deleted file mode 100644 index fae3ea0d..00000000 Binary files a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/STATIONG1.imageset/Contents.json b/Meshtastic/Assets.xcassets/STATIONG1.imageset/Contents.json deleted file mode 100644 index d72dfe5f..00000000 --- a/Meshtastic/Assets.xcassets/STATIONG1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "meshtastic_mesh_device_station_edition_overview 1.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png b/Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png deleted file mode 100644 index a8b03e02..00000000 Binary files a/Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json b/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json deleted file mode 100644 index dc823045..00000000 --- a/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "station-g2.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json b/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json deleted file mode 100644 index 0ecd041c..00000000 --- a/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tbeam.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json b/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json deleted file mode 100644 index b8451344..00000000 --- a/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "t-deck.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json b/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json deleted file mode 100644 index e1adcf61..00000000 --- a/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "t-echo.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json b/Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json deleted file mode 100644 index 7001ca9b..00000000 --- a/Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "thinknode_m1.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json b/Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json deleted file mode 100644 index 81ee0ac1..00000000 --- a/Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "thinknode_m2.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json deleted file mode 100644 index 593dc16e..00000000 --- a/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tlora-c6.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg b/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg deleted file mode 100644 index 8b626638..00000000 --- a/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json deleted file mode 100644 index 33fb9c78..00000000 --- a/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tlora-t3s3-epaper.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json deleted file mode 100644 index a5716fc8..00000000 --- a/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tlora-t3s3-v1.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TLORAV211P6.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAV211P6.imageset/Contents.json deleted file mode 100644 index eb286609..00000000 --- a/Meshtastic/Assets.xcassets/TLORAV211P6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tlora-v2-1-1_6.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TLORAV211P8.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAV211P8.imageset/Contents.json deleted file mode 100644 index c7aff831..00000000 --- a/Meshtastic/Assets.xcassets/TLORAV211P8.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tlora-v2-1-1_8.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json b/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json deleted file mode 100644 index e966c95f..00000000 --- a/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "tracker-t1000-e.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json b/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json deleted file mode 100644 index baffc648..00000000 --- a/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "t-watch-s3.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/UNPHONE.imageset/Contents.json b/Meshtastic/Assets.xcassets/UNPHONE.imageset/Contents.json deleted file mode 100644 index 32acad21..00000000 --- a/Meshtastic/Assets.xcassets/UNPHONE.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "UNPHONE.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png b/Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png deleted file mode 100644 index 06a38558..00000000 Binary files a/Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg b/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg index 3b0a0744..1d2cd87b 100644 --- a/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg +++ b/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg @@ -1,129 +1,160 @@ + class="svg-icon" + style="overflow:hidden;fill:currentColor" + viewBox="0 0 909.87988 546.85529" + version="1.1" + id="svg3" + xml:space="preserve" + width="909.87988" + height="546.85529" + sodipodi:docname="unknown.svg" + inkscape:version="1.4 (e7c3feb1, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> diff --git a/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json b/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json deleted file mode 100644 index 706f7fc3..00000000 --- a/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wio-tracker-wm1110.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json b/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json deleted file mode 100644 index 85d43a9b..00000000 --- a/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "rak-wismeshtap.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json b/Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json deleted file mode 100644 index 08990d2d..00000000 --- a/Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "seeed_xiao_nrf52_kit.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json b/Meshtastic/Assets.xcassets/custom.usb.symbolset/Contents.json similarity index 61% rename from Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json rename to Meshtastic/Assets.xcassets/custom.usb.symbolset/Contents.json index 3046b536..f15ca6d5 100644 --- a/Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/custom.usb.symbolset/Contents.json @@ -1,12 +1,12 @@ { - "images" : [ - { - "filename" : "rak11310.svg", - "idiom" : "universal" - } - ], "info" : { "author" : "xcode", "version" : 1 - } + }, + "symbols" : [ + { + "filename" : "usb-symbol.svg", + "idiom" : "universal" + } + ] } diff --git a/Meshtastic/Assets.xcassets/custom.usb.symbolset/usb-symbol.svg b/Meshtastic/Assets.xcassets/custom.usb.symbolset/usb-symbol.svg new file mode 100644 index 00000000..9f2eeb5f --- /dev/null +++ b/Meshtastic/Assets.xcassets/custom.usb.symbolset/usb-symbol.svg @@ -0,0 +1,46 @@ + + + + + + Small + Medium + Large + + + Ultralight + Regular + Black + Template v.3.0 + + https://github.com/swhitty/SwiftDraw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Extensions/CoreData/DeviceHardwareEntity.swift b/Meshtastic/Extensions/CoreData/DeviceHardwareEntity.swift new file mode 100644 index 00000000..77674a3e --- /dev/null +++ b/Meshtastic/Extensions/CoreData/DeviceHardwareEntity.swift @@ -0,0 +1,12 @@ +// +// DeviceHardwareEntity.swift +// Meshtastic +// +// Created by Jake Bordens on 12/10/25. +// + +import SwiftUI + +extension DeviceHardwareEntity { + +} diff --git a/Meshtastic/Extensions/Image.swift b/Meshtastic/Extensions/Image.swift new file mode 100644 index 00000000..3339e47f --- /dev/null +++ b/Meshtastic/Extensions/Image.swift @@ -0,0 +1,60 @@ +// +// Image.swift +// Meshtastic +// +// Created by jake on 12/5/25. +// + +import SwiftUI + +extension Image { + // Initializer taking a URL +// init?(svgURL: URL, maxSize: CGSize? = nil) { +// guard let svg = SVGKImage(contentsOf: svgURL) else { return nil } +// self.init(svgkImage: svg, maxSize: maxSize) +// } +// +// // Initializer taking Data +// init?(svgData: Data, maxSize: CGSize? = nil) { +// guard let svg = SVGKImage(data: svgData) else { return nil } +// self.init(svgkImage: svg, maxSize: maxSize) +// } +// +// // MARK: - Private Shared Logic +// +//// private init?(svgkImage svg: SVGKImage, maxSize: CGSize?) { +//// guard let root = svg.domDocument?.rootElement as? SVGSVGElement else { return nil } +//// +//// // Calculate the intrinsic size, handling missing width/height attributes +//// // by falling back to the viewBox if necessary. +//// let intrinsicSize: CGSize = { +//// if let w = root.width, w.valueInSpecifiedUnits > 0, +//// let h = root.height, h.valueInSpecifiedUnits > 0 { +//// return CGSize(width: CGFloat(root.width.valueInSpecifiedUnits), +//// height: CGFloat(root.height.valueInSpecifiedUnits)) +//// } else if root.hasAttribute("viewBox") { +//// let viewBox = root.viewBox +//// if viewBox.width > 0, viewBox.height > 0 { +//// return CGSize(width: CGFloat(viewBox.width), +//// height: CGFloat(viewBox.height)) +//// } +//// } +//// return svg.size // Fallback +//// }() +//// +//// guard intrinsicSize.width > 0, intrinsicSize.height > 0 else { return nil } +//// +//// // Apply scaling if maxSize is provided +//// if let maxSize { +//// let scale = min(maxSize.width / intrinsicSize.width, +//// maxSize.height / intrinsicSize.height) +//// svg.size = CGSize(width: intrinsicSize.width * scale, +//// height: intrinsicSize.height * scale) +//// } else { +//// svg.size = intrinsicSize +//// } +//// +//// guard let uiImage = svg.uiImage else { return nil } +//// self.init(uiImage: uiImage) +//// } +} diff --git a/Meshtastic/Extensions/Url.swift b/Meshtastic/Extensions/Url.swift index fab43b98..996a4837 100644 --- a/Meshtastic/Extensions/Url.swift +++ b/Meshtastic/Extensions/Url.swift @@ -63,4 +63,49 @@ extension URL { var creationDate: Date? { return attributes?[.creationDate] as? Date } + + /// Checks if the URL points to a valid file without downloading the body. + /// - Parameter timeout: How long to wait before failing (default: 5 seconds). + /// - Returns: True if the server returns a 200 OK status. + func isValidDownload(timeout: TimeInterval = 5.0) async -> Bool { + var request = URLRequest(url: self) + request.httpMethod = "HEAD" + request.timeoutInterval = timeout + + do { + let (_, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + return false + } + + // Accept 200 (OK). + // Depending on your needs, you might also accept 200...299 + return httpResponse.statusCode == 200 + } catch { + return false + } + } + + /// Checks if the URL points to a valid file (Closure based for older iOS). + func isValidDownload(timeout: TimeInterval = 5.0, completion: @escaping (Bool) -> Void) { + var request = URLRequest(url: self) + request.httpMethod = "HEAD" + request.timeoutInterval = timeout + + let task = URLSession.shared.dataTask(with: request) { _, response, error in + if let _ = error { + completion(false) + return + } + + if let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 { + completion(true) + } else { + completion(false) + } + } + task.resume() + } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 751ddc17..1dceabd4 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -81,6 +81,8 @@ extension UserDefaults { case autoconnectOnDiscovery case manualConnections case testIntEnum + case lastDeviceAPIUpdate + case lastFirmwareAPIUpdate } func reset() { @@ -209,6 +211,11 @@ extension UserDefaults { } } } + @UserDefault(.lastDeviceAPIUpdate, defaultValue: .distantPast) + static var lastDeviceAPIUpdate: Date + + @UserDefault(.lastFirmwareAPIUpdate, defaultValue: .distantPast) + static var lastFirmwareAPIUpdate: Date } enum TestIntEnum: Int, Decodable { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 1e68896b..57fabb64 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -127,6 +127,8 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum) myInfoEntity.rebootCount = Int32(myInfo.rebootCount) myInfoEntity.deviceId = myInfo.deviceID + myInfoEntity.pioEnv = myInfo.pioEnv + do { try context.save() Logger.data.info("💾 Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") @@ -141,6 +143,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO fetchedMyInfo[0].peripheralId = peripheralId fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount) + fetchedMyInfo[0].pioEnv = myInfo.pioEnv do { try context.save() @@ -314,12 +317,14 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.shortName = nodeInfo.user.shortName newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) - newUser.hwDisplayName = dh?.displayName - } + + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "hwModel == %d", newUser.hwModelId) + let fetchedHardware = try context.fetch(fetchRequest) + if let hardwareEntity = fetchedHardware.first { + newUser.hwDisplayName = hardwareEntity.displayName } + newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) if !nodeInfo.user.publicKey.isEmpty { @@ -429,22 +434,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user?.unmessagable = false } } - Task { - Api().loadDeviceHardwareData { (hw: [DeviceHardware]) in - guard !hw.isEmpty, - let firstNode = fetchedNode.first, - let user = firstNode.user else { - Logger.data.error("Error: Required DeviceHardware data is missing or array is empty.") - return - } - - let dh = hw.first(where: { $0.hwModel == user.hwModelId }) - - if let deviceHardware = dh { - firstNode.user?.hwDisplayName = deviceHardware.displayName - } else { - Logger.data.error("No matching hardware model found for ID: \(user.hwModelId, privacy: .public)") - } + + if let user = fetchedNode.first?.user { + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "hwModel == %d", user.hwModelId) + let fetchedHardware = try context.fetch(fetchRequest) + if let hardwareEntity = fetchedHardware.first { + user.hwDisplayName = hardwareEntity.displayName } } } else { diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index 863fb0e9..518973f7 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -133,6 +133,8 @@ bluetooth-central location + UIFileSharingEnabled + UILaunchScreen UIRequiredDeviceCapabilities @@ -158,6 +160,28 @@ UISupportsDocumentBrowser + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + UF2 Firmware + UTTypeIconFiles + + UTTypeIdentifier + com.meshtastic.uf2-firmware + UTTypeTagSpecification + + public.filename-extension + + uf2 + + + + UTImportedTypeDeclarations diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents index a6e5465f..020e5b9f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -73,6 +73,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -118,6 +138,16 @@ + + + + + + + + + + @@ -199,6 +229,7 @@ + diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index d60ed940..106b30c9 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -217,5 +217,7 @@ struct MeshtasticAppleApp: App { .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(appState) .environmentObject(accessoryManager) + .environmentObject(appState.router) + .environmentObject(MeshtasticAPI.shared) } } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 19521601..73db711d 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -25,6 +25,18 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if locationsHandler.backgroundActivity { locationsHandler.backgroundActivity = true } + + if Calendar.current.date(byAdding: .day, value: 1, to: UserDefaults.lastDeviceAPIUpdate)! < Date() { + // lastUpdate is older than 1 day + Task { + Logger.services.info("📋 Device list API data is older than one day, updating...") + try await MeshtasticAPI.shared.refreshDevicesAPIData() + UserDefaults.lastDeviceAPIUpdate = Date() + } + } else { + Logger.services.info("📋 Device list API data update is not needed...") + } + return true } // Lets us show the notification in the app in the foreground diff --git a/Meshtastic/Model/Firmware/FirmwareFile.swift b/Meshtastic/Model/Firmware/FirmwareFile.swift new file mode 100644 index 00000000..acb9c1b0 --- /dev/null +++ b/Meshtastic/Model/Firmware/FirmwareFile.swift @@ -0,0 +1,317 @@ +// +// FirmwareFile.swift +// Meshtastic +// +// Created by jake on 12/13/25. +// + +import Foundation +import SwiftUI +import CoreData + +extension FirmwareFile { + enum FirmwareFileError: Error, LocalizedError { + case invalidFilenamePrefix + case parseError + case unknownFileType + case unknownTarget + case unknownArchitecture + case unknownVersion + case unknownReleaseType + case unknownRemoteURL + + var errorDescription: String? { + switch self { + case .invalidFilenamePrefix: + return "Filename must start with `firmware-`." + case .parseError: + return "Unable to parse the components of the filename (target and version)." + case .unknownFileType: + return "Unknown file type. May not be a firmware file." + case .unknownTarget: + return "Unknown platformio target." + case .unknownArchitecture: + return "Unknown architecture." + case .unknownVersion: + return "Unknown version." + case .unknownReleaseType: + return "Unknown release type (stable/alpha)." + case .unknownRemoteURL: + return "Unknown remote URL." + } + } + } +} + +// Various Enums and constants +extension FirmwareFile { + enum DownloadStatus: Equatable { + case notDownloaded + case downloading + case downloaded + case error(String) + } + + enum FirmwareType: String, Identifiable, CustomStringConvertible { + var id: String { rawValue } + var description: String { return rawValue } + + case uf2 = ".uf2" + case bin = ".bin" + case otaZip = "-ota.zip" + } + + static let localFirmwareStorageURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + static let remoteFirmwareURLPrefix = URL(string: "https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/")! +} + +class FirmwareFile: ObservableObject, Hashable, Equatable { + let localUrl: URL + let remoteUrl: URL? + let versionId: String + let platformioTarget: String + let releaseType: FirmwareRelease.ReleaseType + @Published var status: DownloadStatus + let firmwareType: FirmwareType + let architecure: Architecture + let releaseNotes: String? + + let versionMajor, versionMinor, versionPatch: Int + + init(firmware: FirmwareReleaseEntity, hardware: DeviceHardwareEntity, type: FirmwareType? = nil) throws { + var target: String? + var architecture: Architecture? + + // Thread safe operationt to get the target and architecture + // from the given DeviceHardwareEntity + if let context = hardware.managedObjectContext { + context.performAndWait { + target = hardware.platformioTarget + architecture = hardware.architecture.flatMap { Architecture(rawValue: $0) } + } + } else { + // Detached, not yet inserted NSManagedObject + target = hardware.platformioTarget + architecture = hardware.architecture.flatMap { Architecture(rawValue: $0) } + } + + guard let target else { throw FirmwareFileError.unknownTarget } + self.platformioTarget = target + + guard let architecture else { throw FirmwareFileError.unknownArchitecture } + self.architecure = architecture + + // Thread safe operation to get the versionf`rom the given FirmwareReleaseEntity + var version: String? + var releaseType: FirmwareRelease.ReleaseType? + var releaseNotes: String? + if let context = firmware.managedObjectContext { + context.performAndWait { + version = firmware.versionId + releaseType = firmware.releaseType.flatMap { FirmwareRelease.ReleaseType(rawValue: $0) } + releaseNotes = firmware.releaseNotes + } + } else { + version = firmware.versionId + releaseType = firmware.releaseType.flatMap { FirmwareRelease.ReleaseType(rawValue: $0) } + releaseNotes = firmware.releaseNotes + } + + self.releaseNotes = releaseNotes + + guard let version else { throw FirmwareFileError.unknownVersion } + self.versionId = version + + let cleanString = version.hasPrefix("v") ? version.dropFirst() : Substring(version) + let parts = cleanString.split(separator: ".") + if parts.count >= 3 { + self.versionMajor = Int(parts[0]) ?? 0 + self.versionMinor = Int(parts[1]) ?? 0 + self.versionPatch = Int(parts[2]) ?? 0 + } else { + throw FirmwareFileError.parseError + } + + guard let releaseType else { throw FirmwareFileError.unknownReleaseType } + self.releaseType = releaseType + + // Calculate the filename + // Regarding the force unwrap: validFilenameSuffixes should always return at least one type + let defaultFileType = FirmwareFile.validFilenameSuffixes(forArchitecture: architecture).first! + self.firmwareType = type ?? defaultFileType + let fileNameVersion = versionId.hasPrefix("v") ? String(versionId.dropFirst()) : versionId + let fileName = "firmware-\(target)-\(fileNameVersion)\(firmwareType)" + self.localUrl = FirmwareFile.localFirmwareStorageURL.appendingPathComponent(fileName) + self.remoteUrl = FirmwareFile.remoteFirmwareURLPrefix + .appendingPathComponent("firmware-\(fileNameVersion)") + .appendingPathComponent(fileName) + + if FileManager.default.fileExists(atPath: localUrl.path) { + self.status = .downloaded + } else { + self.status = .notDownloaded + } + } + + init(localFile url: URL) throws { + self.localUrl = url + + let fileName = url.lastPathComponent + + // Check Prefix + guard fileName.hasPrefix("firmware-") else { + throw FirmwareFileError.invalidFilenamePrefix + } + + // Check and Strip Suffix (Extension) + // We strip the prefix and suffix first to isolate "-" + var coreName = String(fileName.dropFirst("firmware-".count)) + + if fileName.hasSuffix("-ota.zip") { + coreName = String(coreName.dropLast("-ota.zip".count)) + self.firmwareType = .otaZip + } else if fileName.hasSuffix(".uf2") { + coreName = String(coreName.dropLast(".uf2".count)) + self.firmwareType = .uf2 + } else if fileName.hasSuffix(".bin") { + coreName = String(coreName.dropLast(".bin".count)) + self.firmwareType = .bin + } else { + // File does not match supported extensions + throw FirmwareFileError.unknownFileType + } + + // Extract Target and Version + // Strategy: We assume the format is Target-Version. + // Since Targets can have hyphens (e.g. "esp32-s3"), but Versions usually don't contain + // the separating hyphen in this specific naming convention, we split by the *last* hyphen. + guard let lastHyphenIndex = coreName.lastIndex(of: "-") else { + throw FirmwareFileError.parseError + } + + let target = String(coreName[..= 3 { + self.versionMajor = Int(parts[0]) ?? 0 + self.versionMinor = Int(parts[1]) ?? 0 + self.versionPatch = Int(parts[2]) ?? 0 + } else { + throw FirmwareFileError.parseError + } + + if !version.hasPrefix("v") { + version = "v" + version + } + + // Validation to ensure we didn't end up with empty strings + guard !target.isEmpty, !version.isEmpty else { + throw FirmwareFileError.parseError + } + + self.versionId = version + self.platformioTarget = target + + if FileManager.default.fileExists(atPath: url.path) { + self.status = .downloaded + } else { + self.status = .notDownloaded + } + + // Look up the architecture for this file + let context = PersistenceController.shared.container.newBackgroundContext() + var architecture: Architecture? + context.performAndWait { + let hardwareFetchRequest = DeviceHardwareEntity.fetchRequest() + hardwareFetchRequest.predicate = NSPredicate(format: "platformioTarget == %@", target) + hardwareFetchRequest.fetchLimit = 1 + let hardware = try? context.fetch(hardwareFetchRequest).first + architecture = hardware?.architecture.flatMap { Architecture(rawValue: $0) } + } + + guard let architecture else { throw FirmwareFileError.unknownArchitecture } + self.architecure = architecture + + // Determine release type + var releaseType: FirmwareRelease.ReleaseType = .unlisted + var releaseNotes: String? + context.performAndWait { + let firmwareFetchRequest = FirmwareReleaseEntity.fetchRequest() + firmwareFetchRequest.predicate = NSPredicate(format: "versionId == %@", version) + firmwareFetchRequest.fetchLimit = 1 + if let firmware = try? context.fetch(firmwareFetchRequest).first { + releaseType = firmware.releaseType.flatMap { FirmwareRelease.ReleaseType(rawValue: $0) } ?? .unlisted + releaseNotes = firmware.releaseNotes + } + } + self.releaseType = releaseType + self.releaseNotes = releaseNotes + + let fileNameVersion = versionId.hasPrefix("v") ? String(versionId.dropFirst()) : versionId + self.remoteUrl = FirmwareFile.remoteFirmwareURLPrefix + .appendingPathComponent("firmware-\(fileNameVersion)") + .appendingPathComponent(fileName) + } + + @MainActor + func download() async throws { + guard let remoteUrl else { + throw FirmwareFileError.unknownRemoteURL + } + Task { + do { + let (tempLocalUrl, response) = try await URLSession.shared.download(from: remoteUrl) + + if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) { + throw URLError(.badServerResponse) + } + + if FileManager.default.fileExists(atPath: localUrl.path) { + try FileManager.default.removeItem(at: localUrl) + } + + try FileManager.default.moveItem(at: tempLocalUrl, to: localUrl) + + self.status = .downloaded + + } catch { + try? FileManager.default.removeItem(at: localUrl) + self.status = .error(error.localizedDescription) + } + } + } + + static func validFilenameSuffixes(forArchitecture: Architecture) -> [FirmwareType] { + switch forArchitecture { + case .esp32, .esp32C3, .esp32S3, .esp32C6: + return [.bin] + case .nrf52840: + return [.uf2, .otaZip] + case .rp2040: + return [.uf2] + } + } + + static func == (lhs: FirmwareFile, rhs: FirmwareFile) -> Bool { + return lhs.localUrl == rhs.localUrl && + lhs.remoteUrl == rhs.remoteUrl && + lhs.versionId == rhs.versionId && + lhs.platformioTarget == rhs.platformioTarget && + lhs.releaseType == rhs.releaseType && + lhs.firmwareType == rhs.firmwareType && + lhs.architecure == rhs.architecure + } + + func hash(into hasher: inout Hasher) { + hasher.combine(localUrl) + hasher.combine(remoteUrl) + hasher.combine(versionId) + hasher.combine(platformioTarget) + hasher.combine(releaseType) + hasher.combine(firmwareType) + hasher.combine(architecure) + } +} + diff --git a/Meshtastic/Model/Firmware/FirmwareViewModel.swift b/Meshtastic/Model/Firmware/FirmwareViewModel.swift new file mode 100644 index 00000000..be8a32a1 --- /dev/null +++ b/Meshtastic/Model/Firmware/FirmwareViewModel.swift @@ -0,0 +1,191 @@ +// +// FirmwareViewModel.swift +// Meshtastic +// +// Created by Jake Bordens on 12/11/25. +// + +import Foundation +import SwiftUI +import CoreData +import OSLog + +extension FirmwareViewModel { + enum FirmwareViewModelError: Error, LocalizedError { + case timedOut(TimeInterval) + case unknownFirmwareVersion + case unableToFindOrCreateEntity + case unknownArchitecture + case unknownPlatformIOTarget + var errorDescription: String? { + switch self { + case .timedOut(let seconds): + return "The operation timed out after \(seconds) seconds." + case .unknownFirmwareVersion: + return "Unknown firmware version." + case .unableToFindOrCreateEntity: + return "Unable to find or create Core Data entity." + case .unknownArchitecture: + return "Unknown architecture." + case .unknownPlatformIOTarget: + return "Unknown platformio target." + } + } + } +} + +class FirmwareViewModel: ObservableObject { + @Published var firmwareFiles: [FirmwareFile] = [] + let hardware: DeviceHardwareEntity + + init(forHardware: DeviceHardwareEntity) { + self.hardware = forHardware + Task { + try? await MeshtasticAPI.shared.refreshFirmwareAPIData() + refresh() + } + } + + func refresh() { + var newFirmwareList = [String: FirmwareFile]() + + // First, loop through all firmware entities and create an entry for those + let context = PersistenceController.shared.container.newBackgroundContext() + context.performAndWait { + let fetchRequest = FirmwareReleaseEntity.fetchRequest() + do { + let firmwareReleases = try context.fetch(fetchRequest) + for release in firmwareReleases { + if let architecture = hardware.architecture.flatMap({ Architecture(rawValue: $0) }) { + for firmwareType in FirmwareFile.validFilenameSuffixes(forArchitecture: architecture) { + let firmwareFile = try FirmwareFile(firmware: release, hardware: hardware, type: firmwareType) + newFirmwareList[firmwareFile.localUrl.lastPathComponent] = firmwareFile + } + } else { + // Just the default + let firmwareFile = try FirmwareFile(firmware: release, hardware: hardware) + newFirmwareList[firmwareFile.localUrl.lastPathComponent] = firmwareFile + } + } + } catch { + Logger.services.error("Error fetching firmware releases: \(error)") + } + } + + // Now look for unlisted files on the filesystem + let fileManager = FileManager.default + var isDirectory: ObjCBool = false + + // 1. Check if directory exists + if !fileManager.fileExists(atPath: FirmwareFile.localFirmwareStorageURL.path, isDirectory: &isDirectory) { + return + } + + // 2. Iterate the files in the folder + do { + let fileURLs = try fileManager.contentsOfDirectory(at: FirmwareFile.localFirmwareStorageURL, includingPropertiesForKeys: nil) + + for url in fileURLs { + do { + let firmwareFile = try FirmwareFile(localFile: url) + if firmwareFile.platformioTarget != hardware.platformioTarget { + // Skip if this is not for the current hardware we are dealing with + continue + } + + if newFirmwareList[firmwareFile.localUrl.lastPathComponent] != nil { + // Already have this one from the Core Data entries + continue + } + newFirmwareList[firmwareFile.localUrl.lastPathComponent] = firmwareFile + } catch { + Logger.services.error("Error parsing local firmware file at \(url.path): \(error)") + } + } + } catch { + Logger.services.error("Error loading firmware files: \(error)") + } + + // Keep the list sorted by version, with deterministic ordering of the firmware type + self.firmwareFiles = newFirmwareList.values.sorted { + if ($0.versionMajor, $0.versionMinor, $0.versionPatch) == ($1.versionMajor, $1.versionMinor, $1.versionPatch) { + // If versions are equal, sort by firmwareType (assuming it's String or Comparable) + return String(describing: $0.firmwareType) < String(describing: $1.firmwareType) + } + return ($0.versionMajor, $0.versionMinor, $0.versionPatch) > ($1.versionMajor, $1.versionMinor, $1.versionPatch) + } + } + + func mostRecentFirmwareVersion(forReleaseType releaseType: FirmwareRelease.ReleaseType) -> String? { + let context = PersistenceController.shared.container.newBackgroundContext() + var versionId: String? + + try? context.performAndWait { + let fetchRequest = FirmwareReleaseEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "releaseType == %@", releaseType.rawValue) + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "versionMajor", ascending: false), + NSSortDescriptor(key: "versionMinor", ascending: false), + NSSortDescriptor(key: "versionPatch", ascending: false)] + fetchRequest.fetchLimit = 1 + do { + if let firmwareRelease = try context.fetch(fetchRequest).first { + versionId = firmwareRelease.versionId + } + } + } + return versionId + } + + func firmwareFiles(forVersionId versionId: String) -> [FirmwareFile] { + return firmwareFiles.filter({ $0.versionId == versionId }) + } + + func mostRecentFirmware(forReleaseType releaseType: FirmwareRelease.ReleaseType) -> [FirmwareFile] { + if let versionId = mostRecentFirmwareVersion(forReleaseType: releaseType) { + return firmwareFiles.filter { $0.releaseType == releaseType && $0.versionId == versionId } + } else { + // Worst case, rely on sorting and only return the first one + let firmwareOfType = firmwareFiles.filter { $0.releaseType == releaseType } + if let singleVersionToReturn = firmwareOfType.first { + return [singleVersionToReturn] + } + } + return [] + } + + func downloadedFirmware(includeInProgressDownloads: Bool = true) -> [FirmwareFile] { + if includeInProgressDownloads { + return firmwareFiles.filter( { $0.status == .downloading || $0.status == .downloaded }) + } else { + return firmwareFiles.filter( { $0.status == .downloaded }) + } + } + + var hasDownloadedFirmware: Bool { + return !downloadedFirmware(includeInProgressDownloads: false).isEmpty + } + + func delete(_ filesToDelete:[FirmwareFile]) { + // 1. Create a bucket for files that were actually deleted + var deletedFiles = Set() + + // 2. Perform Disk I/O + for file in filesToDelete { + do { + try FileManager.default.removeItem(at: file.localUrl) + deletedFiles.insert(file) + } catch { + // Optional: Handle "File not found" as a success so it clears from UI + if (error as NSError).code == NSFileNoSuchFileError { + deletedFiles.insert(file) + } else { + Logger.services.error("Failed to delete \(file.localUrl.path): \(error)") + } + } + } + + // 3. Update State ONCE (Efficient O(n)) + // This triggers the UI update/Publisher only one time + firmwareFiles.removeAll { deletedFiles.contains($0) } + } +} diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c56644a5..2ca9d0e0 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -140,7 +140,7 @@ public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext } } -public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes: Bool) { +public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes: Bool, includeAppLevelData: Bool = false) { let persistenceController = PersistenceController.shared.container for i in 0...persistenceController.managedObjectModel.entities.count-1 { @@ -150,13 +150,17 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes var deleteRequest = NSBatchDeleteRequest(fetchRequest: query) let entityName = entity.name ?? "UNK" - if includeRoutes { - deleteRequest = NSBatchDeleteRequest(fetchRequest: query) - } else if !includeRoutes { - if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) { - deleteRequest = NSBatchDeleteRequest(fetchRequest: query) - } + if !includeRoutes, ["RouteEntity", "LocationEntity"].contains(entityName) { + continue } + + if !includeAppLevelData, ["DeviceHardwareEntity","DeviceHardwareImageEntity", "DeviceHardwareTagEntity"].contains(entityName) { + // These are non-node-specific "app level" data, keep them even when switching nodes + continue + } + + // Execute the delete for this entry + deleteRequest = NSBatchDeleteRequest(fetchRequest: query) do { try context.executeAndMergeChanges(using: deleteRequest) } catch { @@ -299,11 +303,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: newUser.publicKey = newUserMessage.publicKey } - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) - newUser.hwDisplayName = dh?.displayName - } + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "hwModel == %d", newUser.hwModelId) + let fetchedHardware = try context.fetch(fetchRequest) + if let hardwareEntity = fetchedHardware.first { + newUser.hwDisplayName = hardwareEntity.displayName } newNode.user = newUser @@ -412,10 +416,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: fetchedNode[0].user?.pkiEncrypted = true fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey } - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) - fetchedNode[0].user?.hwDisplayName = dh?.displayName + + if let user = fetchedNode.first?.user { + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "hwModel == %d", user.hwModelId) + let fetchedHardware = try context.fetch(fetchRequest) + if let hardwareEntity = fetchedHardware.first { + user.hwDisplayName = hardwareEntity.displayName } } } diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index a49e973e..efe0bf98 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1,1199 +1,1287 @@ -[ - { - "hwModel": 1, - "hwModelSlug": "TLORA_V2", - "platformioTarget": "tlora-v2", - "architecture": "esp32", - "activelySupported": false, - "displayName": "LILYGO T-LoRa V2", - "tags": [ - "LilyGo" - ] - }, - { - "hwModel": 2, - "hwModelSlug": "TLORA_V1", - "platformioTarget": "tlora-v1", - "architecture": "esp32", - "activelySupported": false, - "displayName": "LILYGO T-LoRa V1", - "tags": [ - "LilyGo" - ] - }, - { - "hwModel": 3, - "hwModelSlug": "TLORA_V2_1_1P6", - "platformioTarget": "tlora-v2-1-1_6", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "LILYGO T-LoRa V2.1-1.6", - "tags": [ - "LilyGo" - ], - "images": [ - "tlora-v2-1-1_6.svg" - ] - }, - { - "hwModel": 4, - "hwModelSlug": "TBEAM", - "platformioTarget": "tbeam", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "LILYGO T-Beam", - "tags": [ - "LilyGo" - ], - "images": [ - "tbeam.svg" - ] - }, - { - "hwModel": 5, - "hwModelSlug": "HELTEC_V2_0", - "platformioTarget": "heltec-v2_0", - "architecture": "esp32", - "activelySupported": false, - "displayName": "Heltec V2.0", - "tags": [ - "Heltec" - ] - }, - { - "hwModel": 6, - "hwModelSlug": "TBEAM_V0P7", - "platformioTarget": "tbeam0_7", - "architecture": "esp32", - "activelySupported": false, - "displayName": "LILYGO T-Beam V0.7", - "tags": [ - "LilyGo" - ] - }, - { - "hwModel": 7, - "hwModelSlug": "T_ECHO", - "platformioTarget": "t-echo", - "architecture": "nrf52840", - "supportLevel": 1, - "activelySupported": true, - "displayName": "LILYGO T-Echo", - "tags": [ - "LilyGo" - ], - "images": [ - "t-echo.svg" - ], - "requiresDfu": true, - "hasInkHud": true - }, - { - "hwModel": 8, - "hwModelSlug": "TLORA_V1_1P3", - "platformioTarget": "tlora-v1_3", - "architecture": "esp32", - "activelySupported": false, - "displayName": "LILYGO T-LoRa V1.1-1.3", - "tags": [ - "LilyGo" - ] - }, - { - "hwModel": 9, - "hwModelSlug": "RAK4631", - "platformioTarget": "rak4631", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "RAK WisBlock 4631", - "tags": [ - "RAK" - ], - "images": [ - "rak4631.svg", - "rak4631_case.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 10, - "hwModelSlug": "HELTEC_V2_1", - "platformioTarget": "heltec-v2_1", - "architecture": "esp32", - "activelySupported": false, - "displayName": "Heltec V2.1", - "tags": [ - "Heltec" - ] - }, - { - "hwModel": 11, - "hwModelSlug": "HELTEC_V1", - "platformioTarget": "heltec-v1", - "architecture": "esp32", - "activelySupported": false, - "displayName": "Heltec V1", - "tags": [ - "Heltec" - ] - }, - { - "hwModel": 12, - "hwModelSlug": "LILYGO_TBEAM_S3_CORE", - "platformioTarget": "tbeam-s3-core", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "LILYGO T-Beam Supreme", - "tags": [ - "LilyGo" - ], - "images": [ - "tbeam-s3-core.svg" - ], - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 13, - "hwModelSlug": "RAK11200", - "platformioTarget": "rak11200", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "RAK WisBlock 11200", - "tags": [ - "RAK" - ], - "images": [ - "rak11200.svg" - ] - }, - { - "hwModel": 14, - "hwModelSlug": "NANO_G1", - "platformioTarget": "nano-g1", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Nano G1", - "tags": [ - "B&Q" - ] - }, - { - "hwModel": 15, - "hwModelSlug": "TLORA_V2_1_1P8", - "platformioTarget": "tlora-v2-1-1_8", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "LILYGO T-LoRa V2.1-1.8", - "tags": [ - "LilyGo", - "2.4GHz" - ], - "images": [ - "tlora-v2-1-1_8.svg" - ] - }, - { - "hwModel": 16, - "hwModelSlug": "TLORA_T3_S3", - "platformioTarget": "tlora-t3s3-v1", - "architecture": "esp32-s3", - "activelySupported": true, - "displayName": "LILYGO T-LoRa T3-S3", - "supportLevel": 1, - "tags": [ - "LilyGo" - ], - "images": [ - "tlora-t3s3-v1.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 16, - "hwModelSlug": "TLORA_T3_S3", - "platformioTarget": "tlora-t3s3-epaper", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "LILYGO T-LoRa T3-S3 E-Ink", - "tags": [ - "LilyGo" - ], - "images": [ - "tlora-t3s3-epaper.svg" - ], - "requiresDfu": true, - "hasInkHud": true - }, - { - "hwModel": 17, - "hwModelSlug": "NANO_G1_EXPLORER", - "platformioTarget": "nano-g1-explorer", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Nano G1 Explorer", - "tags": [ - "B&Q" - ] - }, - { - "hwModel": 18, - "hwModelSlug": "NANO_G2_ULTRA", - "platformioTarget": "nano-g2-ultra", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 2, - "displayName": "Nano G2 Ultra", - "tags": [ - "B&Q" - ], - "requiresDfu": true, - "images": [ - "nano-g2-ultra.svg" - ] - }, - { - "hwModel": 21, - "hwModelSlug": "WIO_WM1110", - "platformioTarget": "wio-tracker-wm1110", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Seeed Wio WM1110 Tracker", - "tags": [ - "Seeed" - ], - "images": [ - "wio-tracker-wm1110.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 25, - "hwModelSlug": "STATION_G1", - "platformioTarget": "station-g1", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Station G1", - "tags": [ - "B&Q" - ] - }, - { - "hwModel": 26, - "hwModelSlug": "RAK11310", - "platformioTarget": "rak11310", - "architecture": "rp2040", - "activelySupported": true, - "supportLevel": 2, - "displayName": "RAK WisBlock 11310", - "tags": [ - "RAK" - ], - "images": [ - "rak11310.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 29, - "hwModelSlug": "CANARYONE", - "platformioTarget": "canaryone", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Canary One", - "tags": [ - "Canary" - ], - "requiresDfu": true - }, - { - "hwModel": 30, - "hwModelSlug": "RP2040_LORA", - "platformioTarget": "rp2040-lora", - "architecture": "rp2040", - "activelySupported": true, - "supportLevel": 2, - "displayName": "RP2040 LoRa", - "tags": [ - "Waveshare" - ], - "requiresDfu": true - }, - { - "hwModel": 31, - "hwModelSlug": "STATION_G2", - "platformioTarget": "station-g2", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 2, - "displayName": "Station G2", - "tags": [ - "B&Q" - ], - "requiresDfu": true, - "images": [ - "station-g2.svg" - ], - "partitionScheme": "16MB" - }, - { - "hwModel": 39, - "hwModelSlug": "DIY_V1", - "platformioTarget": "meshtastic-diy-v1", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "DIY V1", - "tags": [ - "DIY" - ], - "images": [ - "diy.svg" - ] - }, - { - "hwModel": 39, - "hwModelSlug": "HYDRA", - "platformioTarget": "hydra", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Hydra", - "tags": [ - "DIY" - ] - }, - { - "hwModel": 41, - "hwModelSlug": "DR_DEV", - "platformioTarget": "meshtastic-dr-dev", - "architecture": "esp32", - "activelySupported": false, - "displayName": "DR-DEV", - "tags": [ - "DIY" - ] - }, - { - "hwModel": 42, - "hwModelSlug": "M5STACK", - "platformioTarget": "m5stack-core", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 3, - "displayName": "M5 Stack", - "tags": [ - "M5Stack" - ] - }, - { - "hwModel": 43, - "hwModelSlug": "HELTEC_V3", - "platformioTarget": "heltec-v3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec V3", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-v3.svg", - "heltec-v3-case.svg" - ], - "partitionScheme": "8MB" - }, - { - "hwModel": 44, - "hwModelSlug": "HELTEC_WSL_V3", - "platformioTarget": "heltec-wsl-v3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Wireless Stick Lite V3", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-wsl-v3.svg" - ], - "partitionScheme": "8MB" - }, - { - "hwModel": 47, - "hwModelSlug": "RPI_PICO", - "platformioTarget": "pico", - "architecture": "rp2040", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Raspberry Pi Pico", - "tags": [ - "RPi", - "DIY" - ], - "requiresDfu": true, - "images": [ - "pico.svg" - ] - }, - { - "hwModel": 47, - "hwModelSlug": "RPI_PICO", - "platformioTarget": "picow", - "architecture": "rp2040", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Raspberry Pi Pico W", - "tags": [ - "RPi", - "DIY" - ], - "requiresDfu": true, - "images": [ - "rpipicow.svg" - ] - }, - { - "hwModel": 48, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER", - "platformioTarget": "heltec-wireless-tracker", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Wireless Tracker V1.1", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-wireless-tracker.svg" - ], - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 58, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0", - "platformioTarget": "heltec-wireless-tracker-V1-0", - "architecture": "esp32-s3", - "activelySupported": false, - "supportLevel": 3, - "displayName": "Heltec Wireless Tracker V1.0", - "images": [ - "heltec-wireless-tracker.svg" - ], - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 49, - "hwModelSlug": "HELTEC_WIRELESS_PAPER", - "platformioTarget": "heltec-wireless-paper", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Wireless Paper", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-wireless-paper.svg" - ], - "hasInkHud": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 50, - "hwModelSlug": "T_DECK", - "platformioTarget": "t-deck", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "LILYGO T-Deck", - "tags": [ - "LilyGo" - ], - "images": [ - "t-deck.svg" - ], - "requiresDfu": true, - "hasMui": true, - "partitionScheme": "16MB" - }, - { - "hwModel": 51, - "hwModelSlug": "T_WATCH_S3", - "platformioTarget": "t-watch-s3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "LILYGO T-Watch S3", - "tags": [ - "LilyGo" - ], - "images": [ - "t-watch-s3.svg" - ], - "partitionScheme": "8MB" - }, - { - "hwModel": 52, - "hwModelSlug": "PICOMPUTER_S3", - "platformioTarget": "picomputer-s3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Pi Computer S3", - "hasMui": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 53, - "hwModelSlug": "HELTEC_HT62", - "platformioTarget": "heltec-ht62-esp32c3-sx1262", - "architecture": "esp32-c3", - "supportLevel": 1, - "activelySupported": true, - "displayName": "Heltec HT62", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-ht62-esp32c3-sx1262.svg" - ] - }, - { - "hwModel": 57, - "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0", - "platformioTarget": "heltec-wireless-paper-v1_0", - "architecture": "esp32-s3", - "activelySupported": false, - "supportLevel": 3, - "tags": [ - "Heltec" - ], - "displayName": "Heltec Wireless Paper V1.0", - "images": [ - "heltec-wireless-paper-v1_0.svg" - ], - "partitionScheme": "8MB" - }, - { - "hwModel": 59, - "hwModelSlug": "UNPHONE", - "platformioTarget": "unphone", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "unPhone", - "requiresDfu": true, - "hasMui": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 48, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER", - "platformioTarget": "tracksenger", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "TrackSenger (small TFT)", - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 48, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER", - "platformioTarget": "tracksenger-lcd", - "architecture": "esp32-s3", - "activelySupported": false, - "supportLevel": 3, - "displayName": "TrackSenger (big TFT)", - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 48, - "hwModelSlug": "HELTEC_WIRELESS_TRACKER", - "platformioTarget": "tracksenger-oled", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "TrackSenger (big OLED)", - "partitionScheme": "8MB" - }, - { - "hwModel": 61, - "hwModelSlug": "CDEBYTE_EORA_S3", - "platformioTarget": "CDEBYTE_EoRa-S3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "EBYTE EoRa-S3", - "tags": [ - "EByte" - ], - "requiresDfu": true - }, - { - "hwModel": 64, - "hwModelSlug": "RADIOMASTER_900_BANDIT_NANO", - "platformioTarget": "radiomaster_900_bandit_nano", - "architecture": "esp32", - "activelySupported": true, - "supportLevel": 2, - "displayName": "RadioMaster 900 Bandit Nano", - "tags": [ - "RadioMaster" - ] - }, - { - "hwModel": 66, - "hwModelSlug": "HELTEC_VISION_MASTER_T190", - "platformioTarget": "heltec-vision-master-t190", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Vision Master T190", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-vision-master-t190.svg" - ], - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 67, - "hwModelSlug": "HELTEC_VISION_MASTER_E213", - "platformioTarget": "heltec-vision-master-e213", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Vision Master E213", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-vision-master-e213.svg" - ], - "requiresDfu": true, - "hasInkHud": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 68, - "hwModelSlug": "HELTEC_VISION_MASTER_E290", - "platformioTarget": "heltec-vision-master-e290", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Vision Master E290", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-vision-master-e290.svg" - ], - "requiresDfu": true, - "hasInkHud": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 69, - "hwModelSlug": "HELTEC_MESH_NODE_T114", - "platformioTarget": "heltec-mesh-node-t114", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec Mesh Node T114", - "tags": [ - "Heltec" - ], - "images": [ - "heltec-mesh-node-t114.svg", - "heltec-mesh-node-t114-case.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 70, - "hwModelSlug": "SENSECAP_INDICATOR", - "platformioTarget": "seeed-sensecap-indicator", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed SenseCAP Indicator", - "tags": [ - "Seeed" - ], - "images": [ - "seeed-sensecap-indicator.svg" - ], - "hasMui": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 71, - "hwModelSlug": "TRACKER_T1000_E", - "platformioTarget": "tracker-t1000-e", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed Card Tracker T1000-E", - "tags": [ - "Seeed" - ], - "images": [ - "tracker-t1000-e.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 81, - "hwModelSlug": "SEEED_XIAO_S3", - "platformioTarget": "seeed-xiao-s3", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 3, - "displayName": "Seeed Xiao ESP32-S3", - "tags": [ - "Seeed" - ], - "images": [ - "seeed-xiao-s3.svg" - ], - "requiresDfu": true, - "partitionScheme": "8MB" - }, - { - "hwModel": 105, - "hwModelSlug": "WISMESH_TAG", - "platformioTarget": "rak_wismeshtag", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "RAK WisMesh Tag", - "tags": [ - "RAK" - ], - "images": [ - "rak_wismesh_tag.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 84, - "hwModelSlug": "WISMESH_TAP", - "platformioTarget": "rak_wismeshtap", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "RAK WisMesh Tap", - "tags": [ - "RAK" - ], - "images": [ - "rak-wismeshtap.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 22, - "hwModelSlug": "WISMESH_HUB", - "platformioTarget": "rak2560", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "RAK WisMesh Repeater", - "tags": [ - "RAK" - ], - "images": [ - "rak2560.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 63, - "hwModelSlug": "NRF52_PROMICRO_DIY", - "platformioTarget": "nrf52_promicro_diy_tcxo", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 3, - "displayName": "NRF52 Pro-micro DIY", - "tags": [ - "DIY" - ], - "images": [ - "promicro.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 88, - "hwModelSlug": "XIAO_NRF52_KIT", - "platformioTarget": "seeed_xiao_nrf52840_kit", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed Xiao NRF52840 Kit", - "tags": [ - "Seeed" - ], - "requiresDfu": true, - "images": [ - "seeed_xiao_nrf52_kit.svg" - ] - }, - { - "hwModel": 89, - "hwModelSlug": "THINKNODE_M1", - "platformioTarget": "thinknode_m1", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "ThinkNode M1", - "tags": [ - "Elecrow" - ], - "requiresDfu": true, - "images": [ - "thinknode_m1.svg" - ], - "hasInkHud": true - }, - { - "hwModel": 107, - "hwModelSlug": "THINKNODE_M5", - "platformioTarget": "thinknode_m5", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "ThinkNode M5", - "tags": [ - "Elecrow" - ], - "requiresDfu": false, - "images": [ - "thinknode_m1.svg" - ], - "hasInkHud": true - }, - { - "hwModel": 90, - "hwModelSlug": "THINKNODE_M2", - "platformioTarget": "thinknode_m2", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "ThinkNode M2", - "tags": [ - "Elecrow" - ], - "requiresDfu": false, - "images": [ - "thinknode_m2.svg" - ] - }, - { - "hwModel": 94, - "hwModelSlug": "HELTEC_MESH_POCKET", - "platformioTarget": "heltec-mesh-pocket-10000", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec MeshPocket", - "tags": [ - "Heltec" - ], - "images": [ - "heltec_mesh_pocket.svg" - ], - "requiresDfu": true, - "hasInkHud": true - }, - { - "hwModel": 95, - "hwModelSlug": "SEEED_SOLAR_NODE", - "platformioTarget": "seeed_solar_node", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed SenseCAP Solar Node", - "tags": [ - "Seeed" - ], - "images": [ - "seeed_solar.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 99, - "hwModelSlug": "SEEED_WIO_TRACKER_L1", - "platformioTarget": "seeed_wio_tracker_L1", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed Wio Tracker L1", - "tags": [ - "Seeed" - ], - "images": [ - "wio_tracker_l1_case.svg" - ], - "requiresDfu": true - }, - { - "hwModel": 100, - "hwModelSlug": "SEEED_WIO_TRACKER_L1_EINK", - "platformioTarget": "seeed_wio_tracker_L1_eink", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Seeed Wio Tracker L1 E-Ink", - "tags": [ - "Seeed" - ], - "requiresDfu": true, - "hasInkHud": true, - "images": [ - "wio_tracker_l1_eink.svg" - ] - }, - { - "hwModel": 96, - "hwModelSlug": "NOMADSTAR_METEOR_PRO", - "platformioTarget": "rak4631_nomadstar_meteor_pro", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "NomadStar Meteor Pro", - "tags": [ - "NomadStar" - ], - "requiresDfu": true, - "images": [ - "meteor_pro.svg" - ] - }, - { - "hwModel": 97, - "hwModelSlug": "CROWPANEL", - "platformioTarget": "elecrow-adv1-43-50-70-tft", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT", - "tags": [ - "Elecrow" - ], - "requiresDfu": true, - "images": [ - "crowpanel_5_0.svg", - "crowpanel_7_0.svg" - ], - "partitionScheme": "16MB", - "hasMui": true - }, - { - "hwModel": 97, - "hwModelSlug": "CROWPANEL", - "platformioTarget": "elecrow-adv-24-28-tft", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Crowpanel Adv 2.4/2.8 TFT", - "tags": [ - "Elecrow" - ], - "requiresDfu": true, - "images": [ - "crowpanel_2_4.svg", - "crowpanel_2_8.svg" - ], - "partitionScheme": "16MB", - "hasMui": true - }, - { - "hwModel": 97, - "hwModelSlug": "CROWPANEL", - "platformioTarget": "elecrow-adv-35-tft", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Crowpanel Adv 3.5 TFT", - "tags": [ - "Elecrow" - ], - "requiresDfu": true, - "images": [ - "crowpanel_3_5.svg" - ], - "partitionScheme": "16MB", - "hasMui": true - }, - { - "hwModel": 101, - "hwModelSlug": "MUZI_R1_NEO", - "platformioTarget": "r1-neo", - "architecture": "nrf52840", - "activelySupported": true, - "supportLevel": 1, - "displayName": "muzi R1 Neo", - "tags": [ - "muzi" - ], - "requiresDfu": true, - "images": [ - "muzi_r1_neo.svg" - ] - }, - { - "hwModel": 102, - "hwModelSlug": "T_DECK_PRO", - "platformioTarget": "t-deck-pro", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "LILYGO T-Deck Pro", - "tags": [ - "LilyGo" - ], - "images": [ - "tdeck_pro.svg" - ], - "requiresDfu": true, - "hasMui": false, - "partitionScheme": "16MB" - }, - { - "hwModel": 103, - "hwModelSlug": "T_LORA_PAGER", - "platformioTarget": "tlora-pager", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "LILYGO T-LoRa Pager", - "tags": [ - "LilyGo" - ], - "requiresDfu": true, - "hasMui": false, - "partitionScheme": "16MB", - "images": [ - "lilygo-tlora-pager.svg" - ] - }, - { - "hwModel": 108, - "hwModelSlug": "HELTEC_MESH_SOLAR", - "platformioTarget": "heltec-mesh-solar", - "architecture": "nrf52840", - "activelySupported": false, - "supportLevel": 1, - "displayName": "Heltec MeshSolar", - "tags": [ - "Heltec" - ], - "requiresDfu": true, - "images": [ - "heltec-mesh-solar.svg" - ] - }, - { - "hwModel": 109, - "hwModelSlug": "T_ECHO_LITE", - "platformioTarget": "t-echo-lite", - "architecture": "nrf52840", - "activelySupported": false, - "supportLevel": 1, - "displayName": "LILYGO T-Echo Lite", - "tags": [ - "LilyGo" - ], - "requiresDfu": true, - "hasInkHud": false, - "images": [ - "techo_lite.svg" - ] - }, - { - "hwModel": 111, - "hwModelSlug": "M5STACK_C6L", - "platformioTarget": "m5stack-unitc6l", - "architecture": "esp32-c6", - "supportLevel": 1, - "activelySupported": true, - "displayName": "M5Stack Unit C6L", - "tags": [ - "M5Stack" - ], - "images": [ - "m5_c6l.svg" - ] - }, - { - "hwModel": 110, - "hwModelSlug": "HELTEC_V4", - "platformioTarget": "heltec-v4", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "Heltec V4", - "tags": [ - "Heltec" - ], - "requiresDfu": true, - "hasMui": false, - "partitionScheme": "16MB", - "images": [ - "heltec_v4.svg" - ] - }, - { - "hwModel": 106, - "hwModelSlug": "RAK3312", - "platformioTarget": "rak3312", - "architecture": "esp32-s3", - "activelySupported": true, - "supportLevel": 1, - "displayName": "RAK3312", - "tags": [ - "RAK" - ], - "requiresDfu": false, - "hasMui": false, - "partitionScheme": "16MB", - "images": [ - "rak_3312.svg" - ] - }, - { - "hwModel": 115, - "hwModelSlug": "THINKNODE_M3", - "platformioTarget": "thinknode_m3", - "architecture": "nrf52840", - "activelySupported": false, - "supportLevel": 1, - "displayName": "ThinkNode M3", - "tags": [ - "Elecrow" - ], - "requiresDfu": true - } -] - +[ + { + "hwModel": 1, + "hwModelSlug": "TLORA_V2", + "platformioTarget": "tlora-v2", + "architecture": "esp32", + "activelySupported": false, + "displayName": "LILYGO T-LoRa V2", + "tags": [ + "LilyGo" + ] + }, + { + "hwModel": 2, + "hwModelSlug": "TLORA_V1", + "platformioTarget": "tlora-v1", + "architecture": "esp32", + "activelySupported": false, + "displayName": "LILYGO T-LoRa V1", + "tags": [ + "LilyGo" + ] + }, + { + "hwModel": 3, + "hwModelSlug": "TLORA_V2_1_1P6", + "platformioTarget": "tlora-v2-1-1_6", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "LILYGO T-LoRa V2.1-1.6", + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-v2-1-1_6.svg" + ] + }, + { + "hwModel": 4, + "hwModelSlug": "TBEAM", + "platformioTarget": "tbeam", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "LILYGO T-Beam", + "tags": [ + "LilyGo" + ], + "images": [ + "tbeam.svg" + ] + }, + { + "hwModel": 5, + "hwModelSlug": "HELTEC_V2_0", + "platformioTarget": "heltec-v2_0", + "architecture": "esp32", + "activelySupported": false, + "displayName": "Heltec V2.0", + "tags": [ + "Heltec" + ] + }, + { + "hwModel": 6, + "hwModelSlug": "TBEAM_V0P7", + "platformioTarget": "tbeam0_7", + "architecture": "esp32", + "activelySupported": false, + "displayName": "LILYGO T-Beam V0.7", + "tags": [ + "LilyGo" + ] + }, + { + "hwModel": 7, + "hwModelSlug": "T_ECHO", + "platformioTarget": "t-echo", + "architecture": "nrf52840", + "supportLevel": 1, + "activelySupported": true, + "displayName": "LILYGO T-Echo", + "tags": [ + "LilyGo" + ], + "images": [ + "t-echo.svg" + ], + "requiresDfu": true, + "hasInkHud": true + }, + { + "hwModel": 8, + "hwModelSlug": "TLORA_V1_1P3", + "platformioTarget": "tlora-v1_3", + "architecture": "esp32", + "activelySupported": false, + "displayName": "LILYGO T-LoRa V1.1-1.3", + "tags": [ + "LilyGo" + ] + }, + { + "hwModel": 9, + "hwModelSlug": "RAK4631", + "platformioTarget": "rak4631", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "RAK WisBlock 4631", + "tags": [ + "RAK" + ], + "images": [ + "rak4631.svg", + "rak4631_case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 10, + "hwModelSlug": "HELTEC_V2_1", + "platformioTarget": "heltec-v2_1", + "architecture": "esp32", + "activelySupported": false, + "displayName": "Heltec V2.1", + "tags": [ + "Heltec" + ] + }, + { + "hwModel": 11, + "hwModelSlug": "HELTEC_V1", + "platformioTarget": "heltec-v1", + "architecture": "esp32", + "activelySupported": false, + "displayName": "Heltec V1", + "tags": [ + "Heltec" + ] + }, + { + "hwModel": 12, + "hwModelSlug": "LILYGO_TBEAM_S3_CORE", + "platformioTarget": "tbeam-s3-core", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-Beam Supreme", + "tags": [ + "LilyGo" + ], + "images": [ + "tbeam-s3-core.svg" + ], + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 13, + "hwModelSlug": "RAK11200", + "platformioTarget": "rak11200", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "RAK WisBlock 11200", + "tags": [ + "RAK" + ], + "images": [ + "rak11200.svg" + ] + }, + { + "hwModel": 14, + "hwModelSlug": "NANO_G1", + "platformioTarget": "nano-g1", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Nano G1", + "tags": [ + "B&Q" + ] + }, + { + "hwModel": 15, + "hwModelSlug": "TLORA_V2_1_1P8", + "platformioTarget": "tlora-v2-1-1_8", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "LILYGO T-LoRa V2.1-1.8", + "tags": [ + "LilyGo", + "2.4GHz" + ], + "images": [ + "tlora-v2-1-1_8.svg" + ] + }, + { + "hwModel": 16, + "hwModelSlug": "TLORA_T3_S3", + "platformioTarget": "tlora-t3s3-v1", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "LILYGO T-LoRa T3-S3", + "supportLevel": 1, + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-v1.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 16, + "hwModelSlug": "TLORA_T3_S3", + "platformioTarget": "tlora-t3s3-epaper", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-LoRa T3-S3 E-Ink", + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-epaper.svg" + ], + "requiresDfu": true, + "hasInkHud": true + }, + { + "hwModel": 17, + "hwModelSlug": "NANO_G1_EXPLORER", + "platformioTarget": "nano-g1-explorer", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Nano G1 Explorer", + "tags": [ + "B&Q" + ] + }, + { + "hwModel": 18, + "hwModelSlug": "NANO_G2_ULTRA", + "platformioTarget": "nano-g2-ultra", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 2, + "displayName": "Nano G2 Ultra", + "tags": [ + "B&Q" + ], + "requiresDfu": true, + "images": [ + "nano-g2-ultra.svg" + ] + }, + { + "hwModel": 21, + "hwModelSlug": "WIO_WM1110", + "platformioTarget": "wio-tracker-wm1110", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Seeed Wio WM1110 Tracker", + "tags": [ + "Seeed" + ], + "images": [ + "wio-tracker-wm1110.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 25, + "hwModelSlug": "STATION_G1", + "platformioTarget": "station-g1", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Station G1", + "tags": [ + "B&Q" + ] + }, + { + "hwModel": 26, + "hwModelSlug": "RAK11310", + "platformioTarget": "rak11310", + "architecture": "rp2040", + "activelySupported": true, + "supportLevel": 2, + "displayName": "RAK WisBlock 11310", + "tags": [ + "RAK" + ], + "images": [ + "rak11310.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 29, + "hwModelSlug": "CANARYONE", + "platformioTarget": "canaryone", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Canary One", + "tags": [ + "Canary" + ], + "requiresDfu": true + }, + { + "hwModel": 30, + "hwModelSlug": "RP2040_LORA", + "platformioTarget": "rp2040-lora", + "architecture": "rp2040", + "activelySupported": true, + "supportLevel": 2, + "displayName": "RP2040 LoRa", + "tags": [ + "Waveshare" + ], + "requiresDfu": true + }, + { + "hwModel": 31, + "hwModelSlug": "STATION_G2", + "platformioTarget": "station-g2", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 2, + "displayName": "Station G2", + "tags": [ + "B&Q" + ], + "requiresDfu": true, + "images": [ + "station-g2.svg" + ], + "partitionScheme": "16MB" + }, + { + "hwModel": 39, + "hwModelSlug": "DIY_V1", + "platformioTarget": "meshtastic-diy-v1", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "DIY V1", + "tags": [ + "DIY" + ], + "images": [ + "diy.svg" + ] + }, + { + "hwModel": 39, + "hwModelSlug": "HYDRA", + "platformioTarget": "hydra", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Hydra", + "tags": [ + "DIY" + ] + }, + { + "hwModel": 41, + "hwModelSlug": "DR_DEV", + "platformioTarget": "meshtastic-dr-dev", + "architecture": "esp32", + "activelySupported": false, + "displayName": "DR-DEV", + "tags": [ + "DIY" + ] + }, + { + "hwModel": 42, + "hwModelSlug": "M5STACK", + "platformioTarget": "m5stack-core", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 3, + "displayName": "M5 Stack", + "tags": [ + "M5Stack" + ] + }, + { + "hwModel": 43, + "hwModelSlug": "HELTEC_V3", + "platformioTarget": "heltec-v3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec V3", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-v3.svg", + "heltec-v3-case.svg" + ], + "partitionScheme": "8MB" + }, + { + "hwModel": 44, + "hwModelSlug": "HELTEC_WSL_V3", + "platformioTarget": "heltec-wsl-v3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Wireless Stick Lite V3", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wsl-v3.svg" + ], + "partitionScheme": "8MB" + }, + { + "hwModel": 47, + "hwModelSlug": "RPI_PICO", + "platformioTarget": "pico", + "architecture": "rp2040", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Raspberry Pi Pico", + "tags": [ + "RPi", + "DIY" + ], + "requiresDfu": true, + "images": [ + "pico.svg" + ] + }, + { + "hwModel": 47, + "hwModelSlug": "RPI_PICO", + "platformioTarget": "picow", + "architecture": "rp2040", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Raspberry Pi Pico W", + "tags": [ + "RPi", + "DIY" + ], + "requiresDfu": true, + "images": [ + "rpipicow.svg" + ] + }, + { + "hwModel": 48, + "hwModelSlug": "HELTEC_WIRELESS_TRACKER", + "platformioTarget": "heltec-wireless-tracker", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Wireless Tracker V1.1", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wireless-tracker.svg" + ], + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 58, + "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0", + "platformioTarget": "heltec-wireless-tracker-V1-0", + "architecture": "esp32-s3", + "activelySupported": false, + "supportLevel": 3, + "displayName": "Heltec Wireless Tracker V1.0", + "images": [ + "heltec-wireless-tracker.svg" + ], + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 49, + "hwModelSlug": "HELTEC_WIRELESS_PAPER", + "platformioTarget": "heltec-wireless-paper", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Wireless Paper", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wireless-paper.svg" + ], + "hasInkHud": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 50, + "hwModelSlug": "T_DECK", + "platformioTarget": "t-deck", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-Deck", + "tags": [ + "LilyGo" + ], + "images": [ + "t-deck.svg" + ], + "requiresDfu": true, + "hasMui": true, + "partitionScheme": "16MB" + }, + { + "hwModel": 51, + "hwModelSlug": "T_WATCH_S3", + "platformioTarget": "t-watch-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "LILYGO T-Watch S3", + "tags": [ + "LilyGo" + ], + "images": [ + "t-watch-s3.svg" + ], + "partitionScheme": "8MB" + }, + { + "hwModel": 52, + "hwModelSlug": "PICOMPUTER_S3", + "platformioTarget": "picomputer-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Pi Computer S3", + "hasMui": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 53, + "hwModelSlug": "HELTEC_HT62", + "platformioTarget": "heltec-ht62-esp32c3-sx1262", + "architecture": "esp32-c3", + "supportLevel": 1, + "activelySupported": true, + "displayName": "Heltec HT62", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-ht62-esp32c3-sx1262.svg" + ] + }, + { + "hwModel": 57, + "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0", + "platformioTarget": "heltec-wireless-paper-v1_0", + "architecture": "esp32-s3", + "activelySupported": false, + "supportLevel": 3, + "tags": [ + "Heltec" + ], + "displayName": "Heltec Wireless Paper V1.0", + "images": [ + "heltec-wireless-paper-v1_0.svg" + ], + "partitionScheme": "8MB" + }, + { + "hwModel": 59, + "hwModelSlug": "UNPHONE", + "platformioTarget": "unphone", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "unPhone", + "requiresDfu": true, + "hasMui": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 48, + "hwModelSlug": "HELTEC_WIRELESS_TRACKER", + "platformioTarget": "tracksenger", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "TrackSenger (small TFT)", + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 48, + "hwModelSlug": "HELTEC_WIRELESS_TRACKER", + "platformioTarget": "tracksenger-lcd", + "architecture": "esp32-s3", + "activelySupported": false, + "supportLevel": 3, + "displayName": "TrackSenger (big TFT)", + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 48, + "hwModelSlug": "HELTEC_WIRELESS_TRACKER", + "platformioTarget": "tracksenger-oled", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "TrackSenger (big OLED)", + "partitionScheme": "8MB" + }, + { + "hwModel": 61, + "hwModelSlug": "CDEBYTE_EORA_S3", + "platformioTarget": "CDEBYTE_EoRa-S3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "EBYTE EoRa-S3", + "tags": [ + "EByte" + ], + "requiresDfu": true + }, + { + "hwModel": 64, + "hwModelSlug": "RADIOMASTER_900_BANDIT_NANO", + "platformioTarget": "radiomaster_900_bandit_nano", + "architecture": "esp32", + "activelySupported": true, + "supportLevel": 2, + "displayName": "RadioMaster 900 Bandit Nano", + "tags": [ + "RadioMaster" + ] + }, + { + "hwModel": 66, + "hwModelSlug": "HELTEC_VISION_MASTER_T190", + "platformioTarget": "heltec-vision-master-t190", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master T190", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-t190.svg" + ], + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 67, + "hwModelSlug": "HELTEC_VISION_MASTER_E213", + "platformioTarget": "heltec-vision-master-e213", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master E213", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e213.svg" + ], + "requiresDfu": true, + "hasInkHud": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 68, + "hwModelSlug": "HELTEC_VISION_MASTER_E290", + "platformioTarget": "heltec-vision-master-e290", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master E290", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e290.svg" + ], + "requiresDfu": true, + "hasInkHud": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 69, + "hwModelSlug": "HELTEC_MESH_NODE_T114", + "platformioTarget": "heltec-mesh-node-t114", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Mesh Node T114", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-mesh-node-t114.svg", + "heltec-mesh-node-t114-case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 70, + "hwModelSlug": "SENSECAP_INDICATOR", + "platformioTarget": "seeed-sensecap-indicator", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed SenseCAP Indicator", + "tags": [ + "Seeed" + ], + "images": [ + "seeed-sensecap-indicator.svg" + ], + "hasMui": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 71, + "hwModelSlug": "TRACKER_T1000_E", + "platformioTarget": "tracker-t1000-e", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Card Tracker T1000-E", + "tags": [ + "Seeed" + ], + "images": [ + "tracker-t1000-e.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 81, + "hwModelSlug": "SEEED_XIAO_S3", + "platformioTarget": "seeed-xiao-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 3, + "displayName": "Seeed Xiao ESP32-S3", + "tags": [ + "Seeed" + ], + "images": [ + "seeed-xiao-s3.svg" + ], + "requiresDfu": true, + "partitionScheme": "8MB" + }, + { + "hwModel": 105, + "hwModelSlug": "WISMESH_TAG", + "platformioTarget": "rak_wismeshtag", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "RAK WisMesh Tag", + "tags": [ + "RAK" + ], + "images": [ + "rak_wismesh_tag.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 84, + "hwModelSlug": "WISMESH_TAP", + "platformioTarget": "rak_wismeshtap", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "RAK WisMesh Tap", + "tags": [ + "RAK" + ], + "images": [ + "rak-wismeshtap.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 22, + "hwModelSlug": "WISMESH_HUB", + "platformioTarget": "rak2560", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "RAK WisMesh Repeater", + "tags": [ + "RAK" + ], + "images": [ + "rak2560.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 63, + "hwModelSlug": "NRF52_PROMICRO_DIY", + "platformioTarget": "nrf52_promicro_diy_tcxo", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 3, + "displayName": "NRF52 Pro-micro DIY", + "tags": [ + "DIY" + ], + "images": [ + "promicro.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 88, + "hwModelSlug": "XIAO_NRF52_KIT", + "platformioTarget": "seeed_xiao_nrf52840_kit", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Xiao NRF52840 Kit", + "tags": [ + "Seeed" + ], + "requiresDfu": true, + "images": [ + "seeed_xiao_nrf52_kit.svg" + ] + }, + { + "hwModel": 89, + "hwModelSlug": "THINKNODE_M1", + "platformioTarget": "thinknode_m1", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "ThinkNode M1", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m1.svg" + ], + "hasInkHud": true + }, + { + "hwModel": 107, + "hwModelSlug": "THINKNODE_M5", + "platformioTarget": "thinknode_m5", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "ThinkNode M5", + "tags": [ + "Elecrow" + ], + "requiresDfu": false, + "images": [ + "thinknode_m1.svg" + ], + "hasInkHud": true + }, + { + "hwModel": 90, + "hwModelSlug": "THINKNODE_M2", + "platformioTarget": "thinknode_m2", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "ThinkNode M2", + "tags": [ + "Elecrow" + ], + "requiresDfu": false, + "images": [ + "thinknode_m2.svg" + ] + }, + { + "hwModel": 93, + "hwModelSlug": "MUZI_BASE", + "platformioTarget": "muzi-base", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "muzi base", + "tags": [ + "muzi" + ], + "requiresDfu": true, + "images": [ + "muzi_base.svg" + ] + }, + { + "hwModel": 94, + "hwModelSlug": "HELTEC_MESH_POCKET", + "platformioTarget": "heltec-mesh-pocket-10000", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec MeshPocket", + "tags": [ + "Heltec" + ], + "images": [ + "heltec_mesh_pocket.svg" + ], + "requiresDfu": true, + "hasInkHud": true, + "key": "HELTEC_MESH_POCKET", + "variant": "10000mAh" + }, + { + "hwModel": 94, + "hwModelSlug": "HELTEC_MESH_POCKET", + "platformioTarget": "heltec-mesh-pocket-5000", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec MeshPocket", + "tags": [ + "Heltec" + ], + "images": [ + "heltec_mesh_pocket.svg" + ], + "requiresDfu": true, + "hasInkHud": true, + "key": "HELTEC_MESH_POCKET", + "variant": "5000mAh" + }, + { + "hwModel": 95, + "hwModelSlug": "SEEED_SOLAR_NODE", + "platformioTarget": "seeed_solar_node", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed SenseCAP Solar Node", + "tags": [ + "Seeed" + ], + "images": [ + "seeed_solar.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 99, + "hwModelSlug": "SEEED_WIO_TRACKER_L1", + "platformioTarget": "seeed_wio_tracker_L1", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Wio Tracker L1", + "tags": [ + "Seeed" + ], + "images": [ + "wio_tracker_l1_case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 100, + "hwModelSlug": "SEEED_WIO_TRACKER_L1_EINK", + "platformioTarget": "seeed_wio_tracker_L1_eink", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Wio Tracker L1 E-Ink", + "tags": [ + "Seeed" + ], + "requiresDfu": true, + "hasInkHud": true, + "images": [ + "wio_tracker_l1_eink.svg" + ] + }, + { + "hwModel": 96, + "hwModelSlug": "NOMADSTAR_METEOR_PRO", + "platformioTarget": "rak4631_nomadstar_meteor_pro", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "NomadStar Meteor Pro", + "tags": [ + "NomadStar" + ], + "requiresDfu": true, + "images": [ + "meteor_pro.svg" + ] + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv1-43-50-70-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_5_0.svg", + "crowpanel_7_0.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-24-28-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 2.4/2.8 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_2_4.svg", + "crowpanel_2_8.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-35-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 3.5 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_3_5.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 101, + "hwModelSlug": "MUZI_R1_NEO", + "platformioTarget": "r1-neo", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "muzi R1 Neo", + "tags": [ + "muzi" + ], + "requiresDfu": true, + "images": [ + "muzi_r1_neo.svg" + ] + }, + { + "hwModel": 102, + "hwModelSlug": "T_DECK_PRO", + "platformioTarget": "t-deck-pro", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-Deck Pro", + "tags": [ + "LilyGo" + ], + "images": [ + "tdeck_pro.svg" + ], + "requiresDfu": true, + "hasMui": false, + "partitionScheme": "16MB" + }, + { + "hwModel": 103, + "hwModelSlug": "T_LORA_PAGER", + "platformioTarget": "tlora-pager", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-LoRa Pager", + "tags": [ + "LilyGo" + ], + "requiresDfu": true, + "hasMui": false, + "partitionScheme": "16MB", + "images": [ + "lilygo-tlora-pager.svg" + ] + }, + { + "hwModel": 108, + "hwModelSlug": "HELTEC_MESH_SOLAR", + "platformioTarget": "heltec-mesh-solar", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "Heltec MeshSolar", + "tags": [ + "Heltec" + ], + "requiresDfu": true, + "images": [ + "heltec-mesh-solar.svg" + ] + }, + { + "hwModel": 109, + "hwModelSlug": "T_ECHO_LITE", + "platformioTarget": "t-echo-lite", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "LILYGO T-Echo Lite", + "tags": [ + "LilyGo" + ], + "requiresDfu": true, + "hasInkHud": false, + "images": [ + "techo_lite.svg" + ] + }, + { + "hwModel": 111, + "hwModelSlug": "M5STACK_C6L", + "platformioTarget": "m5stack-unitc6l", + "architecture": "esp32-c6", + "supportLevel": 1, + "activelySupported": true, + "displayName": "M5Stack Unit C6L", + "tags": [ + "M5Stack" + ], + "images": [ + "m5_c6l.svg" + ] + }, + { + "hwModel": 110, + "hwModelSlug": "HELTEC_V4", + "platformioTarget": "heltec-v4", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec V4", + "tags": [ + "Heltec" + ], + "requiresDfu": true, + "hasMui": false, + "partitionScheme": "16MB", + "images": [ + "heltec_v4.svg" + ] + }, + { + "hwModel": 106, + "hwModelSlug": "RAK3312", + "platformioTarget": "rak3312", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "RAK3312", + "tags": [ + "RAK" + ], + "requiresDfu": false, + "hasMui": false, + "partitionScheme": "16MB", + "images": [ + "rak_3312.svg" + ] + }, + { + "hwModel": 115, + "hwModelSlug": "THINKNODE_M3", + "platformioTarget": "thinknode_m3", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "ThinkNode M3", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m3.svg" + ] + }, + { + "hwModel": 116, + "hwModelSlug": "WISMESH_TAP_V2", + "platformioTarget": "rak_wismesh_tap_v2", + "architecture": "esp32-s3", + "activelySupported": false, + "supportLevel": 1, + "displayName": "RAK WisMesh Tap V2", + "tags": [ + "RAK" + ], + "hasMui": true, + "partitionScheme": "8MB", + "images": [ + "rak-wismesh-tap-v2.svg" + ] + }, + { + "hwModel": 119, + "hwModelSlug": "THINKNODE_M4", + "platformioTarget": "thinknode_m4", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "ThinkNode M4", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m4.svg" + ] + }, + { + "hwModel": 120, + "hwModelSlug": "THINKNODE_M6", + "platformioTarget": "thinknode_m6", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "ThinkNode M6", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m6.svg" + ] + } +] \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/crowpanel_2_4.svg b/Meshtastic/Resources/Devices.xcassets/crowpanel_2_4.svg new file mode 100644 index 00000000..cde67ae6 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/crowpanel_2_4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/crowpanel_2_8.svg b/Meshtastic/Resources/Devices.xcassets/crowpanel_2_8.svg new file mode 100644 index 00000000..446e68d7 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/crowpanel_2_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/crowpanel_3_5.svg b/Meshtastic/Resources/Devices.xcassets/crowpanel_3_5.svg new file mode 100644 index 00000000..a953872e --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/crowpanel_3_5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/crowpanel_5_0.svg b/Meshtastic/Resources/Devices.xcassets/crowpanel_5_0.svg new file mode 100644 index 00000000..9fe209ac --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/crowpanel_5_0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/crowpanel_7_0.svg b/Meshtastic/Resources/Devices.xcassets/crowpanel_7_0.svg new file mode 100644 index 00000000..a4784c22 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/crowpanel_7_0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/diy.svg b/Meshtastic/Resources/Devices.xcassets/diy.svg new file mode 100644 index 00000000..823467ed --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/diy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECHT62.imageset/heltec-ht62-esp32c3-sx1262.svg b/Meshtastic/Resources/Devices.xcassets/heltec-ht62-esp32c3-sx1262.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECHT62.imageset/heltec-ht62-esp32c3-sx1262.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-ht62-esp32c3-sx1262.svg diff --git a/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/heltec-mesh-node-t114-case.svg b/Meshtastic/Resources/Devices.xcassets/heltec-mesh-node-t114-case.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/heltec-mesh-node-t114-case.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-mesh-node-t114-case.svg diff --git a/Meshtastic/Resources/Devices.xcassets/heltec-mesh-node-t114.svg b/Meshtastic/Resources/Devices.xcassets/heltec-mesh-node-t114.svg new file mode 100644 index 00000000..779a8f6a --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/heltec-mesh-node-t114.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/heltec-mesh-solar.svg b/Meshtastic/Resources/Devices.xcassets/heltec-mesh-solar.svg new file mode 100644 index 00000000..6e24f06e --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/heltec-mesh-solar.svg @@ -0,0 +1,7218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg b/Meshtastic/Resources/Devices.xcassets/heltec-v3-case.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-v3-case.svg diff --git a/Meshtastic/Resources/Devices.xcassets/heltec-v3.svg b/Meshtastic/Resources/Devices.xcassets/heltec-v3.svg new file mode 100644 index 00000000..13a5fa64 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/heltec-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/heltec-vision-master-e213.svg b/Meshtastic/Resources/Devices.xcassets/heltec-vision-master-e213.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/heltec-vision-master-e213.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-vision-master-e213.svg diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/heltec-vision-master-e290.svg b/Meshtastic/Resources/Devices.xcassets/heltec-vision-master-e290.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/heltec-vision-master-e290.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-vision-master-e290.svg diff --git a/Meshtastic/Resources/Devices.xcassets/heltec-vision-master-t190.svg b/Meshtastic/Resources/Devices.xcassets/heltec-vision-master-t190.svg new file mode 100644 index 00000000..55db34f9 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/heltec-vision-master-t190.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/heltec-wireless-paper.svg b/Meshtastic/Resources/Devices.xcassets/heltec-wireless-paper.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/heltec-wireless-paper.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-wireless-paper.svg diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/heltec-wireless-tracker.svg b/Meshtastic/Resources/Devices.xcassets/heltec-wireless-tracker.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/heltec-wireless-tracker.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-wireless-tracker.svg diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltec-wsl-v3.svg b/Meshtastic/Resources/Devices.xcassets/heltec-wsl-v3.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltec-wsl-v3.svg rename to Meshtastic/Resources/Devices.xcassets/heltec-wsl-v3.svg diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg b/Meshtastic/Resources/Devices.xcassets/heltec_mesh_pocket.svg similarity index 100% rename from Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg rename to Meshtastic/Resources/Devices.xcassets/heltec_mesh_pocket.svg diff --git a/Meshtastic/Resources/Devices.xcassets/heltec_v4.svg b/Meshtastic/Resources/Devices.xcassets/heltec_v4.svg new file mode 100644 index 00000000..849d056f --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/heltec_v4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/image_manifest.json b/Meshtastic/Resources/Devices.xcassets/image_manifest.json new file mode 100644 index 00000000..a9aa273e --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/image_manifest.json @@ -0,0 +1,182 @@ +{ + "files": { + "heltec-wireless-paper.svg": { + "etag": "\"7a8d8c8e9e712f32ccdb32edcdaebf5e\"" + }, + "seeed-xiao-s3.svg": { + "etag": "\"9d583ddf39288934736d7ac248987524\"" + }, + "heltec-mesh-node-t114.svg": { + "etag": "\"ca927ce170fba26438c557af0de47a1e\"" + }, + "nano-g2-ultra.svg": { + "etag": "\"82575f89ab2f60ffe6c1e009b19b596e\"" + }, + "rak2560.svg": { + "etag": "\"da3e309e4f746f0539e13b1f089411e3\"" + }, + "tlora-v2-1-1_6.svg": { + "etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\"" + }, + "rak4631_case.svg": { + "etag": "\"d141ca68501d83f3ca19ed74cb7ce12e\"" + }, + "heltec-wireless-tracker.svg": { + "etag": "\"bb7143e1b25d1d18d5727baf69a1caed\"" + }, + "heltec-ht62-esp32c3-sx1262.svg": { + "etag": "\"1f4d07a164cbc2cb99f26e4a05a763ea\"" + }, + "pico.svg": { + "etag": "\"9f6b3557953065cce6d56ba6e6d48241\"" + }, + "lilygo-tlora-pager.svg": { + "etag": "\"deb184deacb8006da18ae4751d2e0591\"" + }, + "rak-wismesh-tap-v2.svg": { + "etag": "\"4acc893e184de92446357fcb5bba7812\"" + }, + "heltec-wsl-v3.svg": { + "etag": "\"3ecfe8273cdf0d7dfb04dad6c3fa449a\"" + }, + "rak11200.svg": { + "etag": "\"1a0bfda4331a9bfd29722382a787c700\"" + }, + "seeed_solar.svg": { + "etag": "\"3cc4099ae22ed261b88f1a9f7d235275\"" + }, + "crowpanel_3_5.svg": { + "etag": "\"2d4ee10776f01156dd9570da888be34f\"" + }, + "heltec_mesh_pocket.svg": { + "etag": "\"933aafb0ce3a7b0e1faa67e951bc98ea\"" + }, + "tlora-t3s3-epaper.svg": { + "etag": "\"dfe63532b984fd3f34ce26b38e1f0807\"" + }, + "rak-wismeshtap.svg": { + "etag": "\"8c707dda5c384a10822d3ed785aeb411\"" + }, + "m5_c6l.svg": { + "etag": "\"f17cb7e59a20ccf41243c666cbe54546\"" + }, + "heltec_v4.svg": { + "etag": "\"54e84516a04e1276ca385b41c7aa8b8d\"" + }, + "wio-tracker-wm1110.svg": { + "etag": "\"2dfb221a6a481f957a59b81dfb0dbaf7\"" + }, + "meteor_pro.svg": { + "etag": "\"47ba8e4bc6e224fbd3b09401573549dd\"" + }, + "tlora-t3s3-v1.svg": { + "etag": "\"89510451d52482a475e9cc13503f11a6\"" + }, + "seeed_xiao_nrf52_kit.svg": { + "etag": "\"660b2c3bee85adeccdd5de7ea8d06648\"" + }, + "rak_3312.svg": { + "etag": "\"a2b5c4fdf127868323c8129f84f8691e\"" + }, + "tbeam-s3-core.svg": { + "etag": "\"04c0dab7e74a5c1e647567e150136e5b\"" + }, + "diy.svg": { + "etag": "\"7b670e81e7aace4814887ba681fc9f5b\"" + }, + "promicro.svg": { + "etag": "\"d100b5d3aacf51191d7c4a7eb28db231\"" + }, + "wio_tracker_l1_eink.svg": { + "etag": "\"9074596ea8f08acacfa0ce2c9a48152f\"" + }, + "heltec-vision-master-e290.svg": { + "etag": "\"71b598c2c125b115663ab2d40abcd154\"" + }, + "tracker-t1000-e.svg": { + "etag": "\"b4194c4bb550f8ccbbf205489f37134c\"" + }, + "crowpanel_2_4.svg": { + "etag": "\"3aa8b71d6e9d16f82fddde4ba8b472bd\"" + }, + "muzi_r1_neo.svg": { + "etag": "\"d73a20b71a27e530dc6fbe514f3e9d88\"" + }, + "t-deck.svg": { + "etag": "\"2187caebf4304bb2308c8ee3ca74dd60\"" + }, + "heltec-v3.svg": { + "etag": "\"0e22f17d2a0cd67159a222eb0a01bed1\"" + }, + "crowpanel_2_8.svg": { + "etag": "\"caad57326211a595f18b5f494ae24b59\"" + }, + "t-echo.svg": { + "etag": "\"bd2db1e3f0764478a9841ff568abc807\"" + }, + "heltec-mesh-solar.svg": { + "etag": "\"6d3a4f6266a80493f42c0013e30bb31c\"" + }, + "heltec-v3-case.svg": { + "etag": "\"e935a15ddd7cd116b9c4203f434ff627\"" + }, + "station-g2.svg": { + "etag": "\"f0a75bb77ddfcd8fa4c080caa018e539\"" + }, + "rak4631.svg": { + "etag": "\"3f19ff501b98598546fb6d6e5db1151c\"" + }, + "thinknode_m2.svg": { + "etag": "\"97441ac3a41d23e5e0f4702f5788643d\"" + }, + "tlora-v2-1-1_8.svg": { + "etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\"" + }, + "rak_wismesh_tag.svg": { + "etag": "\"257d649982a6689ec7e7c326c0b4dd2f\"" + }, + "wio_tracker_l1_case.svg": { + "etag": "\"21eccba8adbb33b1df19fe0de79a8734\"" + }, + "rak11310.svg": { + "etag": "\"0761c4ec6607993e6133aca9634cd42e\"" + }, + "heltec-vision-master-t190.svg": { + "etag": "\"7f58cc25f93b203c778a3d6d1c6dc53f\"" + }, + "crowpanel_7_0.svg": { + "etag": "\"c593914e105b75ee978f5ce2e2a27f1c\"" + }, + "crowpanel_5_0.svg": { + "etag": "\"a2920df06d5335284db85a2016c0c6c6\"" + }, + "thinknode_m1.svg": { + "etag": "\"e525d5710fddf72e1626cf35346a6b25\"" + }, + "t-watch-s3.svg": { + "etag": "\"2e474b5742ec392304c939b4ec63d466\"" + }, + "techo_lite.svg": { + "etag": "\"42fdf86393b02396e828149f29295239\"" + }, + "heltec-vision-master-e213.svg": { + "etag": "\"a56c7707865246300bd9e89b1f7155c5\"" + }, + "tbeam.svg": { + "etag": "\"ad1781f30226fbe36bae1cbad7e85bac\"" + }, + "tdeck_pro.svg": { + "etag": "\"6fca0ce5392b390bb7aa690c57ab0fee\"" + }, + "heltec-mesh-node-t114-case.svg": { + "etag": "\"ac7c2abd66e7980db365006332d2b6e7\"" + }, + "seeed-sensecap-indicator.svg": { + "etag": "\"7a0fc63602d8c978b75799032dfda252\"" + }, + "rpipicow.svg": { + "etag": "\"04fd9771add804a62fbfe45b3d360f22\"" + } + }, + "api_hash": "0a8536dc1b62574588e40b0aa1838d19e9d8123c5d2e6872f41c66cacb4add4d" +} \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/lilygo-tlora-pager.svg b/Meshtastic/Resources/Devices.xcassets/lilygo-tlora-pager.svg new file mode 100644 index 00000000..edd7952e --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/lilygo-tlora-pager.svg @@ -0,0 +1,1213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/Devices.xcassets/m5_c6l.svg b/Meshtastic/Resources/Devices.xcassets/m5_c6l.svg new file mode 100644 index 00000000..b0e0a401 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/m5_c6l.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/meteor_pro.svg b/Meshtastic/Resources/Devices.xcassets/meteor_pro.svg new file mode 100644 index 00000000..f268f2df --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/meteor_pro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/muzi_r1_neo.svg b/Meshtastic/Resources/Devices.xcassets/muzi_r1_neo.svg new file mode 100644 index 00000000..2f2cb0bf --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/muzi_r1_neo.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg b/Meshtastic/Resources/Devices.xcassets/nano-g2-ultra.svg similarity index 100% rename from Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg rename to Meshtastic/Resources/Devices.xcassets/nano-g2-ultra.svg diff --git a/Meshtastic/Assets.xcassets/RPIPICO.imageset/pico.svg b/Meshtastic/Resources/Devices.xcassets/pico.svg similarity index 100% rename from Meshtastic/Assets.xcassets/RPIPICO.imageset/pico.svg rename to Meshtastic/Resources/Devices.xcassets/pico.svg diff --git a/Meshtastic/Assets.xcassets/PROMICRO.imageset/promicro.svg b/Meshtastic/Resources/Devices.xcassets/promicro.svg similarity index 100% rename from Meshtastic/Assets.xcassets/PROMICRO.imageset/promicro.svg rename to Meshtastic/Resources/Devices.xcassets/promicro.svg diff --git a/Meshtastic/Resources/Devices.xcassets/rak-wismesh-tap-v2.svg b/Meshtastic/Resources/Devices.xcassets/rak-wismesh-tap-v2.svg new file mode 100644 index 00000000..5ae14e94 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak-wismesh-tap-v2.svg @@ -0,0 +1,745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KLUv/QBYXDYDCkm3qEXwTIKcB5o4h8VhcXhYm2s6GxYUS5RxN03gGG7Pn2i9lrRBk8GNNNAW/kGH +C8EEF0jbtg00gXdd2ykl9Q8kJuCBGUgZuJT2Ca4KoArbBaoZrt+YPb/17eG6i18UR1hObfD9yrQd +r7YOz+TU5realmnVtqpfOnXX72aWPvQi7OVsN2zDdadtzXF9f+AVrYrnELg1FgiggBNYQAQoGEAB +SpCBDpCgxHDZ8EbsAYg92z2L3bODzbygjO2L5OflD0u7jtvVTKvgjwXwar7EaJBJ3nNcx2q4AnqD +K6hhG65j8G0BK8uwOvZfbmb3pWiSNU3LLVxBfL8yzLkI2bg903ELrjcLYDkGo1VbLJZVMk2n4NZh +9H3TaRiWG8mCNWEnyU6SqvmV39RWAaSGb/uttxpmbbxfUN4vgDP2Dds2Csup967pmAUseKvilbFJ +4Pu9PxfcsRynYjsW3SxY05pjlx3Pr+h2tWLPaRXX7qnT7jmmO/ILe8Dtl6FJil83e7l5MexlJ8fy +1GmPrRGbxejOBrfeWWt+NdvW+wVlbPd8ATd+Qf3OdHyr4sgGnjrtvd/VXM3sKfShv+1Yk3Y0VUfT +AjBKdeD7huW3s5rZjo42To5l//2LZFiapkl+fBvNPpJmWJLlF/sompyH3iRH/7tZmh/XPAsU0AAK +IACCFJRUfiVeLJATYrZK9B/ffv/41n//+GaaJelHLpZeHM0w/Pg2iqI5jtyH3f8ejh/fTG5+38mQ ++/7Lj29jJz3nJFl2kodm+YUdWYMFCekro5KNBa/gpkm2e47fDqitRp7RxrXVxhdK22bjfQPA1reG +jW+YjWsXjDauvaODjcNyDBZ/QO2xwbCXA26NCSkAnXUmX9Do+m2mZ5ShRbYZAPScATUbli+4P3eN +rmZ2TTU75Zqdnjrtk566jfO+cXEbt775rXvx+9KXpcjJcCR7D03+8S0sST6anZuh6MnfP76FYTiW +oy/H8eNb+LkXw//597+TvIdmD7v5ybGPIxmaIRdDMtRKWp2eui3UTk3sYye7pvGxj1pZmiWq8bTs +Zkd72tW+dra3nfvuvdc0DS1TTStXc/0+9KIfPelLb3rUpz71ql8961vPf//+/x9+8Y+f/OU3P/rT +n371r5/97edhD334Qy0MxXAMybAMzRAN0TAN1XAN2bANudhFL34xFEVxFEmxFE0RFVExFVVxFVmx +FfnYRz/+MRzFcRzJsRzNUUNHdExHdVxHdmxHTnbSk58MSZEcSZIsSZM0SZRMSZVcSZZsSV720pe/ +DEuxHEuyLEuzNEu0TEu1XEu21NqSm9305jdDUzRHkzRL1ExN1VxN1mxNjnbUox8NUREdURItUTRF +VXRFWbRF+U/DVBPTMSXNFE3TVG1TrnbVi+qokqaKqqmqqqvKqq3K17769a/hKq7jSq7lWq7mqqFr +uqrrurJru3K2s579bMiK7MiSLMmWrMmibMqq7MqybMvytre+/W3Yiu3Yki3Zlq3Zom3aqu3asm3b +oqiJkqiIfrSjrbmW5miGpje5yZblWP6yl21Jkpxkx3Q0R3IUR+2PfGRLcRRD0YutGY5hGHr1o7/8 +4w9Zv/pU5J7t69g1Ufzcdy1c/+rXvvYV1UafdtX8mmiGXmS1Mh1DvmrsX8VS5OjYQ77ytVVZlVVX +VVVVNVVN1VRLtURTNDVTzUzLrpqoyZraaqpqmZYluZZk+UnNm2zKxTZctZKPvVV9avoy9N9r3n/v +fV/RbnZSVHlacpKL/dRKlVRJdVRHVQzVUP3qV73q1a52latcbdOWZddUTdU01dAUNcuUTMl0TEcx +DdMw/elPvU87T1uURVd0RVU0RVMURVHUREtUI9ESJVESHVERDdGPetSjHeVoa7LmaqpmaqYmappm +aZLmaIpmaH6zm9xsS7ZcS7VES7M0S7Icy7EMy19qvuwlL1tyJVUyJVHSJEuSJElyJEUyJD3ZSU62 +IzuuozqmIzqWIzmOoziG4x/92Ec+tuIqqqIqoqIplqJGiqIYil/8ohe72IZsyIZqmIZoaIZkOIai +D3vYw/azf/3qTz/6zV9+8os//P/7z3/rWc/61WuqRz3qTU/60Ys+9N53z33b2a52tac97WYvO9nH +Hvbffe+d95azfOUpRznKTU7ykYf8c68ZaCVtlW5+45vWgMACSsCADphAABE4QQIYUIIQNKAEGJCA +CRhb5J7hePXW7pxuHQIRP+HoTWT/Jj56E4fvePbZang1xyzX/XZGNzy/cueL3tR7r9/5leEKYk0P +xW/6pLdXMyynm6uC+FbBMbuGRd/Fb/JdFbtib92KZflVxVH03+T9XaOc9CY+etf9duaDmr7jFbvN +HTsVaxhbY5vpmDVfNpfqfgEAt1bsJtM0yPdv6qM3iaI3fdKbfvhN4uhNPvSm3r9M06DNZc/qdKvh ++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 +J4KT8hkhYRNBG75U/LerPGXV5dJTKNognpl14W76Q7Z3ZFUsXUze3d5m/H/8+kve9HPPTa9DTsN0 +NF3E0j9UzDYzzEL0Rnc1M89K5HSO7rrtv1vsHlCUqe/3zNAmeT7v2T36f9vbteOfcbX2u+qlxVvv +x93l+9ura4Ui43pWKII2f/iq62Z79acp2Q6Z02y9XDdpuvlv9WnyVPP//vutflZk+zRTK37WXWc0 +0yx289f1Ul3j/UevZMbKVLWs7Uifu1Z/l/vNjLtMN7Hpqd4phxTOh0MBAXI+JpwSBIjgnLScqDjA +kwNQOiNEZN7IG7/6vurVXvrxspQ4MeGEGIacXVpb//PXfzwtKp5N52+m/n3xn2topIaGZyUABXw8 +Kp8RGw0NkxMRHACoOKjgcIE+HhEmKhwQDQ3PCgdEBQ8ATiINjRMOp6XjCdHQOOFUeIiSKprlVr9G +l6t62f+mha1QSnp+5eVWT/1vrz1Fw7K0z36paNueqf+cbsmY+HlqiNllqdcOzfCTt73LoAnJIBZN +UqgE5NEkgw4YSQZpkkEcLlDLR8PBECSL45A0EgMHAWZ3ZZ6fpavYqGb7KSlZk0VNVBp4Tii0fDgt +FTwnFEy1rVbNRy6dZeVEhIgGnUdmLFWFOq68mKxWB3U0jzubqzdXn7PzvWwVSknuzKrQAUn3MONZ ++RILUdWufvEx9XKtP19rEcuV85Rl81zpWqv+Sc39/Sr1ku5nuq75Zx7USe6Mi56leAlR3/n8+ftq +75K+2X7iZcRe9lert4Mi17fVT3mXFr9a/+7ScvVaV99BnfTXXs2fFu+xup/z7Pmb+e3/ulYPisy/ +9aBNcr/u30vbqfX9+/V6qR/7Ocv0oE7STX47vP+9urx0zObc/2yl+/p1eKlN9ROv+5/X3kFR+sNC +fSu/+3zmW7v0M1u/S6e3r6V4UCetK79zZV56zPxL7Dd1ZT0z5eJ+3NP+vfT3f4/azcXe992adlZ9 +dmmRF02rH4q+2hOu8Soqc5ar/ftWe3LUtnIzX9ZurEY1/Tzt8/5iT+ydeXh+Wv6f0hobXas76eo1 +OqLpVfonz+ysbq7sZ4c2uNmFpuyYinfeZm7oaVxs21/865Z8+LaFp1AEbfww036xXpHftasX9fgR +sb7x9atPoeiVrfwU2nxhJpq2G64WqibM7DQsPuv8/qtehaLkuclczApFhcvp+bf7u9rsUvJ5GR50 +QDo87GpFe21U/Svsy+TL7Euz/jLGlBKdjqajw6Lig4onutrD4GqLqrblbG45IkRDw6NyEuk3Y/K3 +9dapnETbnranZq7lmfKoyMJSgYWFRVst2/U1ufbSd2u3t96yE1NfD/P1gHQQZ0TL0jnv4SdzJV+6 +c+K6clnCzMvz1Dszz09EdVc09GRhkFigCBu/89eN9fYdG43vDzFgP+v5H68dVAE5MfeM75Pb/H0f +U5PNe1HR/yJok1Y/Mxn3+S4/eQ9ICGlGzMoMex23+/F8zRH10k2VvSwl8zd2K/v+ellqttzGVd03 +X/zE/eer+3NMMzyz61yz/LLFftvW/yxd/eetv4t+S12hFOtM2/nZtXsQZ+Tw0/xttZFx3xL/Pdnv +cj/dvy/tux/dmtXb/TkP+TTf+LdLRS4gD1CUAjFI5gHa/D2ruaJuPm+mlWsEbZjtk/mqc7M+WQ01 +/yqNoajbFtOYudtsn9v9dy+3/ZbPMS2W5WV383OQx4QGl8vlAnk+cFYOGAGCJXk+I1pYPJ8PyfOB +s+Jp4SQPADgsH6CPR8QJ51NARkb0kIhIdI25hXxQZKd2hVuoh/XbWq+Z2FjIXbjo66WMadmxaq7e +5/Jsy6vrMj6o89de2JzHpbudxe3dmvllnnV8qVc96/igzuJtrWfa7a7faz3lak/O8r3Ey/ribT7E +xi37Xq9cQ+26rNvNcj2os71fqoXPdXqp17DrvjwP6rCWvWO9p3KloVY37jLX5d3VemJZFpqX46Ve +9nQr8+itHA/acPMXn3FuebttdRr3t9d32ptXf/97ebob3xX6WX2qcn+hL7bX51Z+mfJ+X6lnul2h +VupBHcv3Uj3Mcy3NzGK9Ujyu1kovV0e3Sldvx0LPgzq8WOfaqYhlnmn7fs/9y7t1q+25F4sRNX+t +ttsK0XC/snG3br2wU9Hq9lIv73VdM5atI/OW9ev7ntaX66Xe68sr56zWa7wu5txavbJNKUfGaIvt +UvkL8QrvoA2X2lUir3+hHdT5QjvdS/+s3bqFojbbvGVWa6ZFrLNLvaxbZ3aatdvbV/9Q5xGNvXwz +rX/qZe0s5HxOx+pPvcb6W8uaxZ/qt6i1r13tUJR61bruMy16ce/tcqXmr2iSlfDLy8f6Y6jjeXru +yOdXjpo097m1zhTqrOX5W76+FbLifabZ8X+XO9RxR3ZXdy+3TH2md/iZzo16zs2Xv7WZksjMd23u +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 + + + + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/rak-wismeshtap.svg b/Meshtastic/Resources/Devices.xcassets/rak-wismeshtap.svg similarity index 100% rename from Meshtastic/Assets.xcassets/WISMESHTAP.imageset/rak-wismeshtap.svg rename to Meshtastic/Resources/Devices.xcassets/rak-wismeshtap.svg diff --git a/Meshtastic/Resources/Devices.xcassets/rak11200.svg b/Meshtastic/Resources/Devices.xcassets/rak11200.svg new file mode 100644 index 00000000..cac91a24 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak11200.svg @@ -0,0 +1,5374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/Devices.xcassets/rak11310.svg b/Meshtastic/Resources/Devices.xcassets/rak11310.svg new file mode 100644 index 00000000..8f526a47 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak11310.svg @@ -0,0 +1,2339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/Devices.xcassets/rak2560.svg b/Meshtastic/Resources/Devices.xcassets/rak2560.svg new file mode 100644 index 00000000..b8514f01 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak2560.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/rak4631.svg b/Meshtastic/Resources/Devices.xcassets/rak4631.svg new file mode 100644 index 00000000..6dc2957a --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak4631.svg @@ -0,0 +1,3514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/RAK4631.imageset/rak4631_case.svg b/Meshtastic/Resources/Devices.xcassets/rak4631_case.svg similarity index 100% rename from Meshtastic/Assets.xcassets/RAK4631.imageset/rak4631_case.svg rename to Meshtastic/Resources/Devices.xcassets/rak4631_case.svg diff --git a/Meshtastic/Resources/Devices.xcassets/rak_3312.svg b/Meshtastic/Resources/Devices.xcassets/rak_3312.svg new file mode 100644 index 00000000..60f09396 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak_3312.svg @@ -0,0 +1,4826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/Devices.xcassets/rak_wismesh_tag.svg b/Meshtastic/Resources/Devices.xcassets/rak_wismesh_tag.svg new file mode 100644 index 00000000..2e4ac874 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rak_wismesh_tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/Devices.xcassets/rpipicow.svg b/Meshtastic/Resources/Devices.xcassets/rpipicow.svg new file mode 100644 index 00000000..cb4b1f68 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/rpipicow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/seeed-sensecap-indicator.svg b/Meshtastic/Resources/Devices.xcassets/seeed-sensecap-indicator.svg similarity index 100% rename from Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/seeed-sensecap-indicator.svg rename to Meshtastic/Resources/Devices.xcassets/seeed-sensecap-indicator.svg diff --git a/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/seeed-xiao-s3.svg b/Meshtastic/Resources/Devices.xcassets/seeed-xiao-s3.svg similarity index 100% rename from Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/seeed-xiao-s3.svg rename to Meshtastic/Resources/Devices.xcassets/seeed-xiao-s3.svg diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg b/Meshtastic/Resources/Devices.xcassets/seeed_solar.svg similarity index 100% rename from Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg rename to Meshtastic/Resources/Devices.xcassets/seeed_solar.svg diff --git a/Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg b/Meshtastic/Resources/Devices.xcassets/seeed_xiao_nrf52_kit.svg similarity index 100% rename from Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg rename to Meshtastic/Resources/Devices.xcassets/seeed_xiao_nrf52_kit.svg diff --git a/Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg b/Meshtastic/Resources/Devices.xcassets/station-g2.svg similarity index 100% rename from Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg rename to Meshtastic/Resources/Devices.xcassets/station-g2.svg diff --git a/Meshtastic/Assets.xcassets/TDECK.imageset/t-deck.svg b/Meshtastic/Resources/Devices.xcassets/t-deck.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TDECK.imageset/t-deck.svg rename to Meshtastic/Resources/Devices.xcassets/t-deck.svg diff --git a/Meshtastic/Assets.xcassets/TECHO.imageset/t-echo.svg b/Meshtastic/Resources/Devices.xcassets/t-echo.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TECHO.imageset/t-echo.svg rename to Meshtastic/Resources/Devices.xcassets/t-echo.svg diff --git a/Meshtastic/Assets.xcassets/TWATCHS3.imageset/t-watch-s3.svg b/Meshtastic/Resources/Devices.xcassets/t-watch-s3.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TWATCHS3.imageset/t-watch-s3.svg rename to Meshtastic/Resources/Devices.xcassets/t-watch-s3.svg diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam-s3-core.svg b/Meshtastic/Resources/Devices.xcassets/tbeam-s3-core.svg similarity index 100% rename from Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam-s3-core.svg rename to Meshtastic/Resources/Devices.xcassets/tbeam-s3-core.svg diff --git a/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.svg b/Meshtastic/Resources/Devices.xcassets/tbeam.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.svg rename to Meshtastic/Resources/Devices.xcassets/tbeam.svg diff --git a/Meshtastic/Resources/Devices.xcassets/tdeck_pro.svg b/Meshtastic/Resources/Devices.xcassets/tdeck_pro.svg new file mode 100644 index 00000000..bd04f97c --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/tdeck_pro.svg @@ -0,0 +1,1081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/Devices.xcassets/techo_lite.svg b/Meshtastic/Resources/Devices.xcassets/techo_lite.svg new file mode 100644 index 00000000..0645bb3d --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/techo_lite.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg b/Meshtastic/Resources/Devices.xcassets/thinknode_m1.svg similarity index 100% rename from Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg rename to Meshtastic/Resources/Devices.xcassets/thinknode_m1.svg diff --git a/Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg b/Meshtastic/Resources/Devices.xcassets/thinknode_m2.svg similarity index 100% rename from Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg rename to Meshtastic/Resources/Devices.xcassets/thinknode_m2.svg diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/tlora-t3s3-epaper.svg b/Meshtastic/Resources/Devices.xcassets/tlora-t3s3-epaper.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/tlora-t3s3-epaper.svg rename to Meshtastic/Resources/Devices.xcassets/tlora-t3s3-epaper.svg diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/tlora-t3s3-v1.svg b/Meshtastic/Resources/Devices.xcassets/tlora-t3s3-v1.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/tlora-t3s3-v1.svg rename to Meshtastic/Resources/Devices.xcassets/tlora-t3s3-v1.svg diff --git a/Meshtastic/Assets.xcassets/TLORAV211P6.imageset/tlora-v2-1-1_6.svg b/Meshtastic/Resources/Devices.xcassets/tlora-v2-1-1_6.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TLORAV211P6.imageset/tlora-v2-1-1_6.svg rename to Meshtastic/Resources/Devices.xcassets/tlora-v2-1-1_6.svg diff --git a/Meshtastic/Assets.xcassets/TLORAV211P8.imageset/tlora-v2-1-1_8.svg b/Meshtastic/Resources/Devices.xcassets/tlora-v2-1-1_8.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TLORAV211P8.imageset/tlora-v2-1-1_8.svg rename to Meshtastic/Resources/Devices.xcassets/tlora-v2-1-1_8.svg diff --git a/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/tracker-t1000-e.svg b/Meshtastic/Resources/Devices.xcassets/tracker-t1000-e.svg similarity index 100% rename from Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/tracker-t1000-e.svg rename to Meshtastic/Resources/Devices.xcassets/tracker-t1000-e.svg diff --git a/Meshtastic/Assets.xcassets/WIOWM1110.imageset/wio-tracker-wm1110.svg b/Meshtastic/Resources/Devices.xcassets/wio-tracker-wm1110.svg similarity index 100% rename from Meshtastic/Assets.xcassets/WIOWM1110.imageset/wio-tracker-wm1110.svg rename to Meshtastic/Resources/Devices.xcassets/wio-tracker-wm1110.svg diff --git a/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/wio_tracker_l1_case.svg b/Meshtastic/Resources/Devices.xcassets/wio_tracker_l1_case.svg similarity index 100% rename from Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/wio_tracker_l1_case.svg rename to Meshtastic/Resources/Devices.xcassets/wio_tracker_l1_case.svg diff --git a/Meshtastic/Resources/Devices.xcassets/wio_tracker_l1_eink.svg b/Meshtastic/Resources/Devices.xcassets/wio_tracker_l1_eink.svg new file mode 100644 index 00000000..772bf3d9 --- /dev/null +++ b/Meshtastic/Resources/Devices.xcassets/wio_tracker_l1_eink.svg @@ -0,0 +1,4507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/crowpanel_2_4.svg b/Meshtastic/Resources/images/crowpanel_2_4.svg new file mode 100644 index 00000000..cde67ae6 --- /dev/null +++ b/Meshtastic/Resources/images/crowpanel_2_4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/crowpanel_2_8.svg b/Meshtastic/Resources/images/crowpanel_2_8.svg new file mode 100644 index 00000000..446e68d7 --- /dev/null +++ b/Meshtastic/Resources/images/crowpanel_2_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/crowpanel_3_5.svg b/Meshtastic/Resources/images/crowpanel_3_5.svg new file mode 100644 index 00000000..a953872e --- /dev/null +++ b/Meshtastic/Resources/images/crowpanel_3_5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/crowpanel_5_0.svg b/Meshtastic/Resources/images/crowpanel_5_0.svg new file mode 100644 index 00000000..9fe209ac --- /dev/null +++ b/Meshtastic/Resources/images/crowpanel_5_0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/crowpanel_7_0.svg b/Meshtastic/Resources/images/crowpanel_7_0.svg new file mode 100644 index 00000000..a4784c22 --- /dev/null +++ b/Meshtastic/Resources/images/crowpanel_7_0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/diy.svg b/Meshtastic/Resources/images/diy.svg new file mode 100644 index 00000000..823467ed --- /dev/null +++ b/Meshtastic/Resources/images/diy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-ht62-esp32c3-sx1262.svg b/Meshtastic/Resources/images/heltec-ht62-esp32c3-sx1262.svg new file mode 100644 index 00000000..c52534ef --- /dev/null +++ b/Meshtastic/Resources/images/heltec-ht62-esp32c3-sx1262.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-mesh-node-t114-case.svg b/Meshtastic/Resources/images/heltec-mesh-node-t114-case.svg new file mode 100644 index 00000000..b2abe639 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-mesh-node-t114-case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-mesh-node-t114.svg b/Meshtastic/Resources/images/heltec-mesh-node-t114.svg new file mode 100644 index 00000000..779a8f6a --- /dev/null +++ b/Meshtastic/Resources/images/heltec-mesh-node-t114.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-mesh-solar.svg b/Meshtastic/Resources/images/heltec-mesh-solar.svg new file mode 100644 index 00000000..6e24f06e --- /dev/null +++ b/Meshtastic/Resources/images/heltec-mesh-solar.svg @@ -0,0 +1,7218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/heltec-v3-case.svg b/Meshtastic/Resources/images/heltec-v3-case.svg new file mode 100644 index 00000000..1b1d3c55 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-v3-case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-v3.svg b/Meshtastic/Resources/images/heltec-v3.svg new file mode 100644 index 00000000..13a5fa64 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-vision-master-e213.svg b/Meshtastic/Resources/images/heltec-vision-master-e213.svg new file mode 100644 index 00000000..2c1cca09 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-vision-master-e213.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-vision-master-e290.svg b/Meshtastic/Resources/images/heltec-vision-master-e290.svg new file mode 100644 index 00000000..ca7d296a --- /dev/null +++ b/Meshtastic/Resources/images/heltec-vision-master-e290.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-vision-master-t190.svg b/Meshtastic/Resources/images/heltec-vision-master-t190.svg new file mode 100644 index 00000000..55db34f9 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-vision-master-t190.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-wireless-paper.svg b/Meshtastic/Resources/images/heltec-wireless-paper.svg new file mode 100644 index 00000000..cb3f188d --- /dev/null +++ b/Meshtastic/Resources/images/heltec-wireless-paper.svg @@ -0,0 +1 @@ + diff --git a/Meshtastic/Resources/images/heltec-wireless-tracker.svg b/Meshtastic/Resources/images/heltec-wireless-tracker.svg new file mode 100644 index 00000000..a5392595 --- /dev/null +++ b/Meshtastic/Resources/images/heltec-wireless-tracker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec-wsl-v3.svg b/Meshtastic/Resources/images/heltec-wsl-v3.svg new file mode 100644 index 00000000..1741223e --- /dev/null +++ b/Meshtastic/Resources/images/heltec-wsl-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/heltec_mesh_pocket.svg b/Meshtastic/Resources/images/heltec_mesh_pocket.svg new file mode 100644 index 00000000..1af4f5c6 --- /dev/null +++ b/Meshtastic/Resources/images/heltec_mesh_pocket.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/heltec_v4.svg b/Meshtastic/Resources/images/heltec_v4.svg new file mode 100644 index 00000000..849d056f --- /dev/null +++ b/Meshtastic/Resources/images/heltec_v4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/image_manifest.json b/Meshtastic/Resources/images/image_manifest.json new file mode 100644 index 00000000..3ed98012 --- /dev/null +++ b/Meshtastic/Resources/images/image_manifest.json @@ -0,0 +1,191 @@ +{ + "files": { + "tlora-t3s3-v1.svg": { + "etag": "\"89510451d52482a475e9cc13503f11a6\"" + }, + "rak-wismeshtap.svg": { + "etag": "\"8c707dda5c384a10822d3ed785aeb411\"" + }, + "tbeam.svg": { + "etag": "\"ad1781f30226fbe36bae1cbad7e85bac\"" + }, + "heltec-ht62-esp32c3-sx1262.svg": { + "etag": "\"1f4d07a164cbc2cb99f26e4a05a763ea\"" + }, + "t-deck.svg": { + "etag": "\"2187caebf4304bb2308c8ee3ca74dd60\"" + }, + "rak2560.svg": { + "etag": "\"da3e309e4f746f0539e13b1f089411e3\"" + }, + "tracker-t1000-e.svg": { + "etag": "\"b4194c4bb550f8ccbbf205489f37134c\"" + }, + "t-watch-s3.svg": { + "etag": "\"2e474b5742ec392304c939b4ec63d466\"" + }, + "rak11310.svg": { + "etag": "\"0761c4ec6607993e6133aca9634cd42e\"" + }, + "muzi_r1_neo.svg": { + "etag": "\"d73a20b71a27e530dc6fbe514f3e9d88\"" + }, + "muzi_base.svg": { + "etag": "\"d82c0733add18e61809c9a2434bf6148\"" + }, + "pico.svg": { + "etag": "\"9f6b3557953065cce6d56ba6e6d48241\"" + }, + "crowpanel_7_0.svg": { + "etag": "\"c593914e105b75ee978f5ce2e2a27f1c\"" + }, + "heltec-mesh-node-t114-case.svg": { + "etag": "\"ac7c2abd66e7980db365006332d2b6e7\"" + }, + "seeed-xiao-s3.svg": { + "etag": "\"9d583ddf39288934736d7ac248987524\"" + }, + "heltec-mesh-node-t114.svg": { + "etag": "\"ca927ce170fba26438c557af0de47a1e\"" + }, + "rak_wismesh_tag.svg": { + "etag": "\"257d649982a6689ec7e7c326c0b4dd2f\"" + }, + "tbeam-s3-core.svg": { + "etag": "\"04c0dab7e74a5c1e647567e150136e5b\"" + }, + "crowpanel_3_5.svg": { + "etag": "\"2d4ee10776f01156dd9570da888be34f\"" + }, + "rak11200.svg": { + "etag": "\"1a0bfda4331a9bfd29722382a787c700\"" + }, + "heltec_v4.svg": { + "etag": "\"54e84516a04e1276ca385b41c7aa8b8d\"" + }, + "crowpanel_2_8.svg": { + "etag": "\"caad57326211a595f18b5f494ae24b59\"" + }, + "rak4631.svg": { + "etag": "\"3f19ff501b98598546fb6d6e5db1151c\"" + }, + "rak4631_case.svg": { + "etag": "\"d141ca68501d83f3ca19ed74cb7ce12e\"" + }, + "wio_tracker_l1_eink.svg": { + "etag": "\"9074596ea8f08acacfa0ce2c9a48152f\"" + }, + "nano-g2-ultra.svg": { + "etag": "\"82575f89ab2f60ffe6c1e009b19b596e\"" + }, + "techo_lite.svg": { + "etag": "\"42fdf86393b02396e828149f29295239\"" + }, + "rak_3312.svg": { + "etag": "\"a2b5c4fdf127868323c8129f84f8691e\"" + }, + "tlora-t3s3-epaper.svg": { + "etag": "\"dfe63532b984fd3f34ce26b38e1f0807\"" + }, + "tlora-v2-1-1_6.svg": { + "etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\"" + }, + "heltec-wireless-paper.svg": { + "etag": "\"7a8d8c8e9e712f32ccdb32edcdaebf5e\"" + }, + "meteor_pro.svg": { + "etag": "\"47ba8e4bc6e224fbd3b09401573549dd\"" + }, + "heltec-wireless-tracker.svg": { + "etag": "\"bb7143e1b25d1d18d5727baf69a1caed\"" + }, + "thinknode_m4.svg": { + "etag": "\"bf1503cde2927c24cafaaeeb1cada43f\"" + }, + "station-g2.svg": { + "etag": "\"f0a75bb77ddfcd8fa4c080caa018e539\"" + }, + "thinknode_m3.svg": { + "etag": "\"9fbe23b50c26a8c0d5e80a1b9e5bef61\"" + }, + "heltec_mesh_pocket.svg": { + "etag": "\"933aafb0ce3a7b0e1faa67e951bc98ea\"" + }, + "heltec-vision-master-t190.svg": { + "etag": "\"7f58cc25f93b203c778a3d6d1c6dc53f\"" + }, + "diy.svg": { + "etag": "\"7b670e81e7aace4814887ba681fc9f5b\"" + }, + "heltec-vision-master-e213.svg": { + "etag": "\"a56c7707865246300bd9e89b1f7155c5\"" + }, + "seeed_solar.svg": { + "etag": "\"3cc4099ae22ed261b88f1a9f7d235275\"" + }, + "wio_tracker_l1_case.svg": { + "etag": "\"21eccba8adbb33b1df19fe0de79a8734\"" + }, + "heltec-v3.svg": { + "etag": "\"0e22f17d2a0cd67159a222eb0a01bed1\"" + }, + "heltec-wsl-v3.svg": { + "etag": "\"3ecfe8273cdf0d7dfb04dad6c3fa449a\"" + }, + "seeed-sensecap-indicator.svg": { + "etag": "\"7a0fc63602d8c978b75799032dfda252\"" + }, + "t-echo.svg": { + "etag": "\"bd2db1e3f0764478a9841ff568abc807\"" + }, + "tdeck_pro.svg": { + "etag": "\"6fca0ce5392b390bb7aa690c57ab0fee\"" + }, + "lilygo-tlora-pager.svg": { + "etag": "\"deb184deacb8006da18ae4751d2e0591\"" + }, + "thinknode_m1.svg": { + "etag": "\"e525d5710fddf72e1626cf35346a6b25\"" + }, + "heltec-vision-master-e290.svg": { + "etag": "\"71b598c2c125b115663ab2d40abcd154\"" + }, + "rpipicow.svg": { + "etag": "\"04fd9771add804a62fbfe45b3d360f22\"" + }, + "promicro.svg": { + "etag": "\"d100b5d3aacf51191d7c4a7eb28db231\"" + }, + "wio-tracker-wm1110.svg": { + "etag": "\"2dfb221a6a481f957a59b81dfb0dbaf7\"" + }, + "crowpanel_2_4.svg": { + "etag": "\"3aa8b71d6e9d16f82fddde4ba8b472bd\"" + }, + "heltec-v3-case.svg": { + "etag": "\"e935a15ddd7cd116b9c4203f434ff627\"" + }, + "tlora-v2-1-1_8.svg": { + "etag": "\"7a9de7eff40aab166d5ab9f251dedaaa\"" + }, + "crowpanel_5_0.svg": { + "etag": "\"a2920df06d5335284db85a2016c0c6c6\"" + }, + "seeed_xiao_nrf52_kit.svg": { + "etag": "\"660b2c3bee85adeccdd5de7ea8d06648\"" + }, + "m5_c6l.svg": { + "etag": "\"f17cb7e59a20ccf41243c666cbe54546\"" + }, + "heltec-mesh-solar.svg": { + "etag": "\"6d3a4f6266a80493f42c0013e30bb31c\"" + }, + "rak-wismesh-tap-v2.svg": { + "etag": "\"4acc893e184de92446357fcb5bba7812\"" + }, + "thinknode_m2.svg": { + "etag": "\"97441ac3a41d23e5e0f4702f5788643d\"" + } + }, + "api_hash": "5fcbe7d3ead1dc1156bccfa4747231615d2a8825d2eac8b34f220a3f04a48155" +} \ No newline at end of file diff --git a/Meshtastic/Resources/images/lilygo-tlora-pager.svg b/Meshtastic/Resources/images/lilygo-tlora-pager.svg new file mode 100644 index 00000000..edd7952e --- /dev/null +++ b/Meshtastic/Resources/images/lilygo-tlora-pager.svg @@ -0,0 +1,1213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/m5_c6l.svg b/Meshtastic/Resources/images/m5_c6l.svg new file mode 100644 index 00000000..b0e0a401 --- /dev/null +++ b/Meshtastic/Resources/images/m5_c6l.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/meteor_pro.svg b/Meshtastic/Resources/images/meteor_pro.svg new file mode 100644 index 00000000..f268f2df --- /dev/null +++ b/Meshtastic/Resources/images/meteor_pro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/muzi_base.svg b/Meshtastic/Resources/images/muzi_base.svg new file mode 100644 index 00000000..7742acd0 --- /dev/null +++ b/Meshtastic/Resources/images/muzi_base.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/muzi_r1_neo.svg b/Meshtastic/Resources/images/muzi_r1_neo.svg new file mode 100644 index 00000000..2f2cb0bf --- /dev/null +++ b/Meshtastic/Resources/images/muzi_r1_neo.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/nano-g2-ultra.svg b/Meshtastic/Resources/images/nano-g2-ultra.svg new file mode 100644 index 00000000..6dbe47af --- /dev/null +++ b/Meshtastic/Resources/images/nano-g2-ultra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/pico.svg b/Meshtastic/Resources/images/pico.svg new file mode 100644 index 00000000..82ce6526 --- /dev/null +++ b/Meshtastic/Resources/images/pico.svg @@ -0,0 +1,2956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/promicro.svg b/Meshtastic/Resources/images/promicro.svg new file mode 100644 index 00000000..3dc26021 --- /dev/null +++ b/Meshtastic/Resources/images/promicro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rak-wismesh-tap-v2.svg b/Meshtastic/Resources/images/rak-wismesh-tap-v2.svg new file mode 100644 index 00000000..5ae14e94 --- /dev/null +++ b/Meshtastic/Resources/images/rak-wismesh-tap-v2.svg @@ -0,0 +1,745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KLUv/QBYXDYDCkm3qEXwTIKcB5o4h8VhcXhYm2s6GxYUS5RxN03gGG7Pn2i9lrRBk8GNNNAW/kGH +C8EEF0jbtg00gXdd2ykl9Q8kJuCBGUgZuJT2Ca4KoArbBaoZrt+YPb/17eG6i18UR1hObfD9yrQd +r7YOz+TU5realmnVtqpfOnXX72aWPvQi7OVsN2zDdadtzXF9f+AVrYrnELg1FgiggBNYQAQoGEAB +SpCBDpCgxHDZ8EbsAYg92z2L3bODzbygjO2L5OflD0u7jtvVTKvgjwXwar7EaJBJ3nNcx2q4AnqD +K6hhG65j8G0BK8uwOvZfbmb3pWiSNU3LLVxBfL8yzLkI2bg903ELrjcLYDkGo1VbLJZVMk2n4NZh +9H3TaRiWG8mCNWEnyU6SqvmV39RWAaSGb/uttxpmbbxfUN4vgDP2Dds2Csup967pmAUseKvilbFJ +4Pu9PxfcsRynYjsW3SxY05pjlx3Pr+h2tWLPaRXX7qnT7jmmO/ILe8Dtl6FJil83e7l5MexlJ8fy +1GmPrRGbxejOBrfeWWt+NdvW+wVlbPd8ATd+Qf3OdHyr4sgGnjrtvd/VXM3sKfShv+1Yk3Y0VUfT +AjBKdeD7huW3s5rZjo42To5l//2LZFiapkl+fBvNPpJmWJLlF/sompyH3iRH/7tZmh/XPAsU0AAK +IACCFJRUfiVeLJATYrZK9B/ffv/41n//+GaaJelHLpZeHM0w/Pg2iqI5jtyH3f8ejh/fTG5+38mQ ++/7Lj29jJz3nJFl2kodm+YUdWYMFCekro5KNBa/gpkm2e47fDqitRp7RxrXVxhdK22bjfQPA1reG +jW+YjWsXjDauvaODjcNyDBZ/QO2xwbCXA26NCSkAnXUmX9Do+m2mZ5ShRbYZAPScATUbli+4P3eN +rmZ2TTU75Zqdnjrtk566jfO+cXEbt775rXvx+9KXpcjJcCR7D03+8S0sST6anZuh6MnfP76FYTiW +oy/H8eNb+LkXw//597+TvIdmD7v5ybGPIxmaIRdDMtRKWp2eui3UTk3sYye7pvGxj1pZmiWq8bTs +Zkd72tW+dra3nfvuvdc0DS1TTStXc/0+9KIfPelLb3rUpz71ql8961vPf//+/x9+8Y+f/OU3P/rT +n371r5/97edhD334Qy0MxXAMybAMzRAN0TAN1XAN2bANudhFL34xFEVxFEmxFE0RFVExFVVxFVmx +FfnYRz/+MRzFcRzJsRzNUUNHdExHdVxHdmxHTnbSk58MSZEcSZIsSZM0SZRMSZVcSZZsSV720pe/ +DEuxHEuyLEuzNEu0TEu1XEu21NqSm9305jdDUzRHkzRL1ExN1VxN1mxNjnbUox8NUREdURItUTRF +VXRFWbRF+U/DVBPTMSXNFE3TVG1TrnbVi+qokqaKqqmqqqvKqq3K17769a/hKq7jSq7lWq7mqqFr +uqrrurJru3K2s579bMiK7MiSLMmWrMmibMqq7MqybMvytre+/W3Yiu3Yki3Zlq3Zom3aqu3asm3b +oqiJkqiIfrSjrbmW5miGpje5yZblWP6yl21Jkpxkx3Q0R3IUR+2PfGRLcRRD0YutGY5hGHr1o7/8 +4w9Zv/pU5J7t69g1Ufzcdy1c/+rXvvYV1UafdtX8mmiGXmS1Mh1DvmrsX8VS5OjYQ77ytVVZlVVX +VVVVNVVN1VRLtURTNDVTzUzLrpqoyZraaqpqmZYluZZk+UnNm2zKxTZctZKPvVV9avoy9N9r3n/v +fV/RbnZSVHlacpKL/dRKlVRJdVRHVQzVUP3qV73q1a52latcbdOWZddUTdU01dAUNcuUTMl0TEcx +DdMw/elPvU87T1uURVd0RVU0RVMURVHUREtUI9ESJVESHVERDdGPetSjHeVoa7LmaqpmaqYmappm +aZLmaIpmaH6zm9xsS7ZcS7VES7M0S7Icy7EMy19qvuwlL1tyJVUyJVHSJEuSJElyJEUyJD3ZSU62 +IzuuozqmIzqWIzmOoziG4x/92Ec+tuIqqqIqoqIplqJGiqIYil/8ohe72IZsyIZqmIZoaIZkOIai +D3vYw/azf/3qTz/6zV9+8os//P/7z3/rWc/61WuqRz3qTU/60Ys+9N53z33b2a52tac97WYvO9nH +Hvbffe+d95azfOUpRznKTU7ykYf8c68ZaCVtlW5+45vWgMACSsCADphAABE4QQIYUIIQNKAEGJCA +CRhb5J7hePXW7pxuHQIRP+HoTWT/Jj56E4fvePbZang1xyzX/XZGNzy/cueL3tR7r9/5leEKYk0P +xW/6pLdXMyynm6uC+FbBMbuGRd/Fb/JdFbtib92KZflVxVH03+T9XaOc9CY+etf9duaDmr7jFbvN +HTsVaxhbY5vpmDVfNpfqfgEAt1bsJtM0yPdv6qM3iaI3fdKbfvhN4uhNPvSm3r9M06DNZc/qdKvh ++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 +J4KT8hkhYRNBG75U/LerPGXV5dJTKNognpl14W76Q7Z3ZFUsXUze3d5m/H/8+kve9HPPTa9DTsN0 +NF3E0j9UzDYzzEL0Rnc1M89K5HSO7rrtv1vsHlCUqe/3zNAmeT7v2T36f9vbteOfcbX2u+qlxVvv +x93l+9ura4Ui43pWKII2f/iq62Z79acp2Q6Z02y9XDdpuvlv9WnyVPP//vutflZk+zRTK37WXWc0 +0yx289f1Ul3j/UevZMbKVLWs7Uifu1Z/l/vNjLtMN7Hpqd4phxTOh0MBAXI+JpwSBIjgnLScqDjA +kwNQOiNEZN7IG7/6vurVXvrxspQ4MeGEGIacXVpb//PXfzwtKp5N52+m/n3xn2topIaGZyUABXw8 +Kp8RGw0NkxMRHACoOKjgcIE+HhEmKhwQDQ3PCgdEBQ8ATiINjRMOp6XjCdHQOOFUeIiSKprlVr9G +l6t62f+mha1QSnp+5eVWT/1vrz1Fw7K0z36paNueqf+cbsmY+HlqiNllqdcOzfCTt73LoAnJIBZN +UqgE5NEkgw4YSQZpkkEcLlDLR8PBECSL45A0EgMHAWZ3ZZ6fpavYqGb7KSlZk0VNVBp4Tii0fDgt +FTwnFEy1rVbNRy6dZeVEhIgGnUdmLFWFOq68mKxWB3U0jzubqzdXn7PzvWwVSknuzKrQAUn3MONZ ++RILUdWufvEx9XKtP19rEcuV85Rl81zpWqv+Sc39/Sr1ku5nuq75Zx7USe6Mi56leAlR3/n8+ftq +75K+2X7iZcRe9lert4Mi17fVT3mXFr9a/+7ScvVaV99BnfTXXs2fFu+xup/z7Pmb+e3/ulYPisy/ +9aBNcr/u30vbqfX9+/V6qR/7Ocv0oE7STX47vP+9urx0zObc/2yl+/p1eKlN9ROv+5/X3kFR+sNC +fSu/+3zmW7v0M1u/S6e3r6V4UCetK79zZV56zPxL7Dd1ZT0z5eJ+3NP+vfT3f4/azcXe992adlZ9 +dmmRF02rH4q+2hOu8Soqc5ar/ftWe3LUtnIzX9ZurEY1/Tzt8/5iT+ydeXh+Wv6f0hobXas76eo1 +OqLpVfonz+ysbq7sZ4c2uNmFpuyYinfeZm7oaVxs21/865Z8+LaFp1AEbfww036xXpHftasX9fgR +sb7x9atPoeiVrfwU2nxhJpq2G64WqibM7DQsPuv8/qtehaLkuclczApFhcvp+bf7u9rsUvJ5GR50 +QDo87GpFe21U/Svsy+TL7Euz/jLGlBKdjqajw6Lig4onutrD4GqLqrblbG45IkRDw6NyEuk3Y/K3 +9dapnETbnranZq7lmfKoyMJSgYWFRVst2/U1ufbSd2u3t96yE1NfD/P1gHQQZ0TL0jnv4SdzJV+6 +c+K6clnCzMvz1Dszz09EdVc09GRhkFigCBu/89eN9fYdG43vDzFgP+v5H68dVAE5MfeM75Pb/H0f +U5PNe1HR/yJok1Y/Mxn3+S4/eQ9ICGlGzMoMex23+/F8zRH10k2VvSwl8zd2K/v+ellqttzGVd03 +X/zE/eer+3NMMzyz61yz/LLFftvW/yxd/eetv4t+S12hFOtM2/nZtXsQZ+Tw0/xttZFx3xL/Pdnv +cj/dvy/tux/dmtXb/TkP+TTf+LdLRS4gD1CUAjFI5gHa/D2ruaJuPm+mlWsEbZjtk/mqc7M+WQ01 +/yqNoajbFtOYudtsn9v9dy+3/ZbPMS2W5WV383OQx4QGl8vlAnk+cFYOGAGCJXk+I1pYPJ8PyfOB +s+Jp4SQPADgsH6CPR8QJ51NARkb0kIhIdI25hXxQZKd2hVuoh/XbWq+Z2FjIXbjo66WMadmxaq7e +5/Jsy6vrMj6o89de2JzHpbudxe3dmvllnnV8qVc96/igzuJtrWfa7a7faz3lak/O8r3Ey/ribT7E +xi37Xq9cQ+26rNvNcj2os71fqoXPdXqp17DrvjwP6rCWvWO9p3KloVY37jLX5d3VemJZFpqX46Ve +9nQr8+itHA/acPMXn3FuebttdRr3t9d32ptXf/97ebob3xX6WX2qcn+hL7bX51Z+mfJ+X6lnul2h +VupBHcv3Uj3Mcy3NzGK9Ujyu1kovV0e3Sldvx0LPgzq8WOfaqYhlnmn7fs/9y7t1q+25F4sRNX+t +ttsK0XC/snG3br2wU9Hq9lIv73VdM5atI/OW9ev7ntaX66Xe68sr56zWa7wu5txavbJNKUfGaIvt +UvkL8QrvoA2X2lUir3+hHdT5QjvdS/+s3bqFojbbvGVWa6ZFrLNLvaxbZ3aatdvbV/9Q5xGNvXwz +rX/qZe0s5HxOx+pPvcb6W8uaxZ/qt6i1r13tUJR61bruMy16ce/tcqXmr2iSlfDLy8f6Y6jjeXru +yOdXjpo097m1zhTqrOX5W76+FbLifabZ8X+XO9RxR3ZXdy+3TH2md/iZzo16zs2Xv7WZksjMd23u +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 + + + + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rak-wismeshtap.svg b/Meshtastic/Resources/images/rak-wismeshtap.svg new file mode 100644 index 00000000..34e77876 --- /dev/null +++ b/Meshtastic/Resources/images/rak-wismeshtap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rak11200.svg b/Meshtastic/Resources/images/rak11200.svg new file mode 100644 index 00000000..cac91a24 --- /dev/null +++ b/Meshtastic/Resources/images/rak11200.svg @@ -0,0 +1,5374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/rak11310.svg b/Meshtastic/Resources/images/rak11310.svg new file mode 100644 index 00000000..8f526a47 --- /dev/null +++ b/Meshtastic/Resources/images/rak11310.svg @@ -0,0 +1,2339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/rak2560.svg b/Meshtastic/Resources/images/rak2560.svg new file mode 100644 index 00000000..b8514f01 --- /dev/null +++ b/Meshtastic/Resources/images/rak2560.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rak4631.svg b/Meshtastic/Resources/images/rak4631.svg new file mode 100644 index 00000000..6dc2957a --- /dev/null +++ b/Meshtastic/Resources/images/rak4631.svg @@ -0,0 +1,3514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/rak4631_case.svg b/Meshtastic/Resources/images/rak4631_case.svg new file mode 100644 index 00000000..a0b2bbb8 --- /dev/null +++ b/Meshtastic/Resources/images/rak4631_case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rak_3312.svg b/Meshtastic/Resources/images/rak_3312.svg new file mode 100644 index 00000000..60f09396 --- /dev/null +++ b/Meshtastic/Resources/images/rak_3312.svg @@ -0,0 +1,4826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/rak_wismesh_tag.svg b/Meshtastic/Resources/images/rak_wismesh_tag.svg new file mode 100644 index 00000000..2e4ac874 --- /dev/null +++ b/Meshtastic/Resources/images/rak_wismesh_tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/rpipicow.svg b/Meshtastic/Resources/images/rpipicow.svg new file mode 100644 index 00000000..cb4b1f68 --- /dev/null +++ b/Meshtastic/Resources/images/rpipicow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/seeed-sensecap-indicator.svg b/Meshtastic/Resources/images/seeed-sensecap-indicator.svg new file mode 100644 index 00000000..f7bf9db0 --- /dev/null +++ b/Meshtastic/Resources/images/seeed-sensecap-indicator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/seeed-xiao-s3.svg b/Meshtastic/Resources/images/seeed-xiao-s3.svg new file mode 100644 index 00000000..04e97fe0 --- /dev/null +++ b/Meshtastic/Resources/images/seeed-xiao-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/seeed_solar.svg b/Meshtastic/Resources/images/seeed_solar.svg new file mode 100644 index 00000000..3f2b5d47 --- /dev/null +++ b/Meshtastic/Resources/images/seeed_solar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/seeed_xiao_nrf52_kit.svg b/Meshtastic/Resources/images/seeed_xiao_nrf52_kit.svg new file mode 100644 index 00000000..95f7211b --- /dev/null +++ b/Meshtastic/Resources/images/seeed_xiao_nrf52_kit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/station-g2.svg b/Meshtastic/Resources/images/station-g2.svg new file mode 100644 index 00000000..8d2e0aed --- /dev/null +++ b/Meshtastic/Resources/images/station-g2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/t-deck.svg b/Meshtastic/Resources/images/t-deck.svg new file mode 100644 index 00000000..cdc53c5d --- /dev/null +++ b/Meshtastic/Resources/images/t-deck.svg @@ -0,0 +1 @@ +QWERTYIUPOASDFGHKJLaltZXCVBMN \ No newline at end of file diff --git a/Meshtastic/Resources/images/t-echo.svg b/Meshtastic/Resources/images/t-echo.svg new file mode 100644 index 00000000..e178a50f --- /dev/null +++ b/Meshtastic/Resources/images/t-echo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/t-watch-s3.svg b/Meshtastic/Resources/images/t-watch-s3.svg new file mode 100644 index 00000000..19084c19 --- /dev/null +++ b/Meshtastic/Resources/images/t-watch-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tbeam-s3-core.svg b/Meshtastic/Resources/images/tbeam-s3-core.svg new file mode 100644 index 00000000..f42e6d2c --- /dev/null +++ b/Meshtastic/Resources/images/tbeam-s3-core.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tbeam.svg b/Meshtastic/Resources/images/tbeam.svg new file mode 100644 index 00000000..cd0475c6 --- /dev/null +++ b/Meshtastic/Resources/images/tbeam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tdeck_pro.svg b/Meshtastic/Resources/images/tdeck_pro.svg new file mode 100644 index 00000000..bd04f97c --- /dev/null +++ b/Meshtastic/Resources/images/tdeck_pro.svg @@ -0,0 +1,1081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/techo_lite.svg b/Meshtastic/Resources/images/techo_lite.svg new file mode 100644 index 00000000..0645bb3d --- /dev/null +++ b/Meshtastic/Resources/images/techo_lite.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/thinknode_m1.svg b/Meshtastic/Resources/images/thinknode_m1.svg new file mode 100644 index 00000000..27e21a0b --- /dev/null +++ b/Meshtastic/Resources/images/thinknode_m1.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Resources/images/thinknode_m2.svg b/Meshtastic/Resources/images/thinknode_m2.svg new file mode 100644 index 00000000..5e5a0e3c --- /dev/null +++ b/Meshtastic/Resources/images/thinknode_m2.svg @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/thinknode_m3.svg b/Meshtastic/Resources/images/thinknode_m3.svg new file mode 100644 index 00000000..ce5ce6fc --- /dev/null +++ b/Meshtastic/Resources/images/thinknode_m3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/thinknode_m4.svg b/Meshtastic/Resources/images/thinknode_m4.svg new file mode 100644 index 00000000..a5050859 --- /dev/null +++ b/Meshtastic/Resources/images/thinknode_m4.svg @@ -0,0 +1 @@ +T&HStatusFunction \ No newline at end of file diff --git a/Meshtastic/Resources/images/tlora-t3s3-epaper.svg b/Meshtastic/Resources/images/tlora-t3s3-epaper.svg new file mode 100644 index 00000000..6f2e8452 --- /dev/null +++ b/Meshtastic/Resources/images/tlora-t3s3-epaper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tlora-t3s3-v1.svg b/Meshtastic/Resources/images/tlora-t3s3-v1.svg new file mode 100644 index 00000000..1f8847d4 --- /dev/null +++ b/Meshtastic/Resources/images/tlora-t3s3-v1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tlora-v2-1-1_6.svg b/Meshtastic/Resources/images/tlora-v2-1-1_6.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/Meshtastic/Resources/images/tlora-v2-1-1_6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tlora-v2-1-1_8.svg b/Meshtastic/Resources/images/tlora-v2-1-1_8.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/Meshtastic/Resources/images/tlora-v2-1-1_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/tracker-t1000-e.svg b/Meshtastic/Resources/images/tracker-t1000-e.svg new file mode 100644 index 00000000..6f7a06c9 --- /dev/null +++ b/Meshtastic/Resources/images/tracker-t1000-e.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/images/wio-tracker-wm1110.svg b/Meshtastic/Resources/images/wio-tracker-wm1110.svg new file mode 100644 index 00000000..15ace5c5 --- /dev/null +++ b/Meshtastic/Resources/images/wio-tracker-wm1110.svg @@ -0,0 +1 @@ +LoRaWI FILEDRESETGNSSBLE \ No newline at end of file diff --git a/Meshtastic/Resources/images/wio_tracker_l1_case.svg b/Meshtastic/Resources/images/wio_tracker_l1_case.svg new file mode 100644 index 00000000..5104c74e --- /dev/null +++ b/Meshtastic/Resources/images/wio_tracker_l1_case.svg @@ -0,0 +1,710 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Resources/images/wio_tracker_l1_eink.svg b/Meshtastic/Resources/images/wio_tracker_l1_eink.svg new file mode 100644 index 00000000..772bf3d9 --- /dev/null +++ b/Meshtastic/Resources/images/wio_tracker_l1_eink.svg @@ -0,0 +1,4507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Views/Debugging/CoreDataBrowser.swift b/Meshtastic/Views/Debugging/CoreDataBrowser.swift new file mode 100644 index 00000000..bf6bf0ac --- /dev/null +++ b/Meshtastic/Views/Debugging/CoreDataBrowser.swift @@ -0,0 +1,297 @@ +// +// CoreDataBrowser.swift +// Meshtastic +// +// Created by Jake Bordens on 12/9/25. +// + +import SwiftUI +import CoreData +import SwiftDraw +// MARK: - 1. Root Browser (The Menu) + +struct CoreDataBrowser: View { + @Environment(\.managedObjectContext) private var viewContext + + var entityNames: [String] { + // extract all entities from the model attached to the context + return viewContext.persistentStoreCoordinator? + .managedObjectModel.entitiesByName.keys.sorted() ?? [] + } + + var body: some View { + List { + Section(header: Text("Entities")) { + if entityNames.isEmpty { + Text("No Entities Found") + .foregroundColor(.secondary) + } else { + ForEach(entityNames, id: \.self) { name in + NavigationLink(destination: DynamicEntityListView(entityName: name)) { + Label(name, systemImage: "tablecells") + } + } + } + } + } + .navigationTitle("Database Browser") + } +} + +// MARK: - 2. Dynamic List View (Fetch Request by Name) + +/// Lists entities based on a String Entity Name, not a compile-time Type. +struct DynamicEntityListView: View { + let entityName: String + @FetchRequest var fetchRequest: FetchedResults + + init(entityName: String) { + self.entityName = entityName + + // Construct a fetch request for the base NSManagedObject + let request = NSFetchRequest(entityName: entityName) + + // We need a default sort descriptor for FetchRequest to work happily. + // Sorting by objectID keeps them in insertion/creation order usually. + request.sortDescriptors = [NSSortDescriptor(key: "objectID", ascending: true)] + + self._fetchRequest = FetchRequest(fetchRequest: request) + } + + var body: some View { + List(fetchRequest, id: \.objectID) { object in + EntityRow(object: object) + } + .navigationTitle(entityName) + .overlay { + if fetchRequest.isEmpty { + ContentUnavailableView("Table Empty", systemImage: "tray", description: Text("No records found for \(entityName)")) + } + } + } +} + +// MARK: - 3. Relationship List (In-Memory List) + +/// Used when navigating to a To-Many relationship (data is already in memory/faulted) +struct RelationshipListView: View { + let title: String + let objects: [NSManagedObject] + + var body: some View { + List(objects, id: \.objectID) { object in + EntityRow(object: object) + } + .navigationTitle(title) + } +} + +// MARK: - 4. Shared Row View + +struct EntityRow: View { + @ObservedObject var object: NSManagedObject + + var body: some View { + NavigationLink(destination: EntityDetailView(object: object)) { + VStack(alignment: .leading) { + Text(object.debugDisplayName) + .font(.headline) + .lineLimit(1) + Text(object.objectID.uriRepresentation().lastPathComponent) + .font(.caption) + .foregroundColor(.secondary) + } + } + } +} + +// MARK: - 5. Detail View (The Introspector) + +struct EntityDetailView: View { + @ObservedObject var object: NSManagedObject + + var attributes: [String: NSAttributeDescription] { + return object.entity.attributesByName + } + + var relationships: [String: NSRelationshipDescription] { + return object.entity.relationshipsByName + } + + var body: some View { + Form { + Section(header: Text("Metadata")) { + LabeledContent("Object ID", value: object.objectID.uriRepresentation().lastPathComponent) + LabeledContent("Entity Name", value: object.entity.name ?? "Unknown") + } + + Section(header: Text("Attributes")) { + ForEach(attributes.keys.sorted(), id: \.self) { key in + if let value = object.value(forKey: key) { + AttributeRow(key: key, value: value, type: attributes[key]?.attributeType) + } else { + LabeledContent(key, value: "nil") + .foregroundColor(.secondary) + } + } + } + + if !relationships.isEmpty { + Section(header: Text("Relationships")) { + ForEach(relationships.keys.sorted(), id: \.self) { key in + RelationshipNavigationRow(key: key, object: object) + } + } + } + } + .navigationTitle("Details") + .navigationBarTitleDisplayMode(.inline) + } +} + +// MARK: - 6. Relationship Navigation Logic + +struct RelationshipNavigationRow: View { + let key: String + @ObservedObject var object: NSManagedObject + + var body: some View { + let value = object.value(forKey: key) + + if let set = value as? Set { + // To-Many (Unordered) + NavigationLink { + RelationshipListView( + title: key, + objects: set.sorted { $0.debugDisplayName < $1.debugDisplayName } + ) + } label: { + HStack { + Text(key) + Spacer() + Text("\(set.count)") + .foregroundColor(.secondary) + Image(systemName: "folder") + .foregroundColor(.blue) + } + } + .disabled(set.isEmpty) + } else if let orderedSet = value as? NSOrderedSet { + // To-Many (Ordered) + let array = orderedSet.array as? [NSManagedObject] ?? [] + NavigationLink { + RelationshipListView(title: key, objects: array) + } label: { + HStack { + Text(key) + Spacer() + Text("\(array.count)") + .foregroundColor(.secondary) + Image(systemName: "folder") + .foregroundColor(.blue) + } + } + .disabled(array.isEmpty) + } else if let singleObject = value as? NSManagedObject { + // To-One + NavigationLink { + EntityDetailView(object: singleObject) + } label: { + HStack { + Text(key) + Spacer() + Text(singleObject.debugDisplayName) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.secondary) + .font(.caption) + } + } + } else { + // Nil + HStack { + Text(key) + Spacer() + Text("nil") + .foregroundColor(.secondary) + } + } + } +} + +// MARK: - 7. Attribute Formatter + +struct AttributeRow: View { + let key: String + let value: Any + let type: NSAttributeType? + + var body: some View { + VStack(alignment: .leading) { + Text(key).font(.caption).foregroundColor(.secondary) + content + } + .padding(.vertical, 2) + } + + @ViewBuilder + var content: some View { + if let type = type { + switch type { + case .booleanAttributeType: + if let boolVal = value as? Bool { + Label(boolVal ? "True" : "False", systemImage: boolVal ? "checkmark.circle.fill" : "xmark.circle") + .foregroundColor(boolVal ? .green : .red) + } + case .dateAttributeType: + if let date = value as? Date { + Text(date.formatted(date: .abbreviated, time: .standard)) + } + case .binaryDataAttributeType: + if key == "svgData", let data = value as? Data, let svg = SVG(data: data) { + // Magic field name telliing us this is an SVG + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 200, maxHeight: 200) + } else if let data = value as? Data { + Text("Binary Data (\(data.count) bytes)") + .font(.system(.body, design: .monospaced)) + } + case .transformableAttributeType: + Text(String(describing: value)) + .font(.caption) + .lineLimit(3) + default: + Text(String(describing: value)) + } + } else { + Text(String(describing: value)) + } + } +} + +// MARK: - 8. Smart Name Helper + +extension NSManagedObject { + var debugDisplayName: String { + let keys = self.entity.attributesByName.keys + // Heuristic to find a displayable title + let preferredKeys = ["name", "title", "fullName", "username", "email", "identifier", "uuid", "id"] + + for key in preferredKeys { + if keys.contains(key), let val = self.value(forKey: key) as? String, !val.isEmpty { + return val + } + } + + // Fallback: use first string property found + for key in keys { + if let val = self.value(forKey: key) as? String, !val.isEmpty { + return val + } + } + + return "Unnamed Entity" + } +} diff --git a/Meshtastic/Views/Helpers/DeviceHardwareImage.swift b/Meshtastic/Views/Helpers/DeviceHardwareImage.swift new file mode 100644 index 00000000..9711a054 --- /dev/null +++ b/Meshtastic/Views/Helpers/DeviceHardwareImage.swift @@ -0,0 +1,175 @@ +// +// DeviceHardwareImage.swift +// Meshtastic +// +// Created by jake on 12/6/25. +// + +import SwiftUI +import CoreData +import SwiftDraw + +struct DeviceHardwareImage: View where T: BinaryInteger, T: CVarArg { + @Environment(\.managedObjectContext) var context + @FetchRequest var hardware: FetchedResults + @EnvironmentObject var meshtasticAPI: MeshtasticAPI + + // This closure lets the caller define modifiers on the Image + @State private var gridSize: CGSize = .zero + + init(hwId: T) { + + let predicate = NSPredicate(format: "hwModel == %d", hwId) + _hardware = FetchRequest( + entity: DeviceHardwareEntity.entity(), + sortDescriptors: [NSSortDescriptor(key: "hwModelSlug", ascending: true)], + predicate: predicate, + animation: .default + ) + } + + var potentialImages: [DeviceHardwareImageEntity] { + var returnImages = [DeviceHardwareImageEntity]() + var seenFileNames = Set() + for item in hardware { + if let imageList = item.images as? Set { + for image in imageList { + if image.svgData != nil { + let name = image.fileName ?? "" + if !seenFileNames.contains(name) { + seenFileNames.insert(name) + returnImages.append(image) + } + } + if returnImages.count >= 4 { + break + } + } + } + } + + // Sort to keep the order somewhat deterministic + return returnImages.sorted(by: {$0.fileName ?? "" < $1.fileName ?? ""}) + } + + var body: some View { + // 1. Define the footprint. + // We use Color.clear so it takes up space but is invisible. + Color.clear + .aspectRatio(1, contentMode: .fit) // Enforce square aspect ratio (or change as needed) + // 2. Measure the size of this footprint using the new modifier + .onGeometryChange(for: CGSize.self) { proxy in + proxy.size + } action: { newValue in + gridSize = newValue + } + // 3. Draw the actual content on top using the measured size + .overlay { + let images = self.potentialImages + if images.count > 0, gridSize != .zero { + content(size: gridSize, images: self.potentialImages) + } else if meshtasticAPI.isLoadingDeviceList { + ProgressView() + } else { + EmptyView() + } + } + } + + @ViewBuilder + private func content(size: CGSize, images: [DeviceHardwareImageEntity]) -> some View { + let spacing: CGFloat = 10.0 + switch images.count { + case 0: + Image("UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size.width, height: size.height) + + case 1: + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size.width, height: size.height) + } + case 2: + HStack(spacing: spacing) { + ForEach(0..<2, id: \.self) { i in + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: (size.width - 2) / 2, + height: size.height) + } + } + } + + case 3: + HStack(spacing: spacing) { + // Big image on the Left + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: (size.width * 0.6) - 1, + height: size.height) + } + + // Two stacked on the Right + VStack(spacing: spacing) { + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, maxHeight: .infinity) // Flex fill + } + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, maxHeight: .infinity) // Flex fill + } + } + .frame(width: (size.width * 0.4) - 1, + height: size.height) + } + + default: // 4 items + let halfWidth = (size.width - 2) / 2 + let halfHeight = (size.height - 2) / 2 + + VStack(spacing: spacing) { + HStack(spacing: spacing) { + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: halfWidth, height: halfHeight) + } + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: halfWidth, height: halfHeight) + } + } + HStack(spacing: spacing) { + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: halfWidth, height: halfHeight) + } + if let svgData = images[0].svgData, let svg = SVG(data: svgData) { + SVGView(svg: svg) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: halfWidth, height: halfHeight) + } + } + } + } + } +} diff --git a/Meshtastic/Views/Helpers/SupportedHardwareBadge.swift b/Meshtastic/Views/Helpers/SupportedHardwareBadge.swift new file mode 100644 index 00000000..00a32699 --- /dev/null +++ b/Meshtastic/Views/Helpers/SupportedHardwareBadge.swift @@ -0,0 +1,63 @@ +// +// SwiftUIView.swift +// Meshtastic +// +// Created by Jake Bordens on 12/10/25. +// + +import SwiftUI + +struct SupportedHardwareBadge: View where T: BinaryInteger, T: CVarArg { + let hwModelId: T + + @Environment(\.managedObjectContext) var context + @FetchRequest var hardware: FetchedResults + @EnvironmentObject var meshtasticAPI: MeshtasticAPI + + init(hwModelId: T) { + self.hwModelId = hwModelId + let predicate = NSPredicate(format: "hwModel == %d", hwModelId) + _hardware = FetchRequest( + entity: DeviceHardwareEntity.entity(), + sortDescriptors: [NSSortDescriptor(key: "hwModelSlug", ascending: true)], + predicate: predicate, + animation: .default + ) + } + + var body: some View { + switch hardware.count { + case 1: + let device = hardware[0] + VStack { + Image(systemName: device.activelySupported ? "checkmark.seal.fill" : "x.circle") + .font(.largeTitle) + .foregroundStyle(device.activelySupported ? .green : .red) + Text( device.activelySupported ? "Supported" : "Unsupported") + .foregroundStyle(.gray) + .font(.caption2) + } + + default: + if meshtasticAPI.isLoadingDeviceList { + // Still loading the database from the API + VStack { + ProgressView() + Text("Loading") + .foregroundStyle(.gray) + .font(.caption2) + } + } else { + // Can't find this hardware in the database + VStack { + Image(systemName:"questionmark.circle.fill") + .font(.largeTitle) + .foregroundStyle(.gray) + Text("Unknown") + .foregroundStyle(.gray) + .font(.caption2) + } + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 3c20a9e2..9cae9a5d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -13,7 +13,8 @@ struct NodeInfoItem: View { @ObservedObject var node: NodeInfoEntity @State private var currentDevice: DeviceHardware? - + @EnvironmentObject var meshtasticAPI: MeshtasticAPI + var body: some View { if let user = node.user { ViewThatFits(in: .horizontal) { @@ -35,37 +36,29 @@ struct NodeInfoItem: View { Spacer() } VStack(alignment: .center) { - HStack { - if user.hardwareImage != "UNSET" { - Image(user.hardwareImage ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 150) - .cornerRadius(5) - } else { - Image(systemName: "person.crop.circle.badge.questionmark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) - .cornerRadius(5) - } - } +// HStack { + DeviceHardwareImage(hwId: user.hwModelId) + .frame(width: 100, height: 100) + .cornerRadius(5) +// if let image = try? meshtasticAPI.imageForNode(hwModelId: user.hwModelId) { +// image +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(maxHeight: 150) +// .cornerRadius(5) +// } else { +// Image(systemName: "person.crop.circle.badge.questionmark") +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: 75, height: 75) +// .cornerRadius(5) +// } +// } .accessibilityElement(children: .combine) } Spacer() } .accessibilityElement(children: .combine) - .onAppear { - Api().loadDeviceHardwareData { (hw) in - for device in hw { - let currentHardware = node.user?.hwModel ?? "UNSET" - let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "").uppercased() - if deviceString == currentHardware { - currentDevice = device - } - } - } - } } .listRowSeparator(.hidden) HStack { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 2f10c2af..a7d50c47 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -159,9 +159,12 @@ struct AppSettings: View { Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)") } } - clearCoreDataDatabase(context: context, includeRoutes: true) - clearNotifications() - context.refreshAllObjects() + Task { @MainActor in + clearCoreDataDatabase(context: context, includeRoutes: true, includeAppLevelData: true) + clearNotifications() + try? await MeshtasticAPI.shared.refreshDevicesAPIData() + context.refreshAllObjects() + } } } Button { diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 2ad978b5..b85c03e4 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -20,8 +20,8 @@ struct PowerConfig: View { @State private var lsSecs = 300 @State private var minWakeSecs = 10 - @State private var currentDevice: DeviceHardware? - + @State private var architecture: Architecture? + @State private var hasChanges: Bool = false @FocusState private var isFocused: Bool @@ -30,7 +30,7 @@ struct PowerConfig: View { ConfigHeader(title: "Power Config", config: \.powerConfig, node: node, onAppear: setPowerValues) Section { - if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) { + if let architecture, (architecture == .esp32 || architecture == .esp32S3) || (architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) { Toggle(isOn: $isPowerSaving) { Label("Power Saving", systemImage: "bolt") Text("Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.") @@ -51,7 +51,7 @@ struct PowerConfig: View { } header: { Text("Power") } - if currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3 { + if let architecture, architecture == .esp32 || architecture == .esp32S3 { Section { Toggle(isOn: $adcOverride) { Text("ADC Override") @@ -123,15 +123,15 @@ struct PowerConfig: View { } } .onFirstAppear { - Api().loadDeviceHardwareData { (hw) in - for device in hw { - let currentHardware = node?.user?.hwModel ?? "UNSET" - let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { - currentDevice = device - } + if let userHwModel = node?.user?.hwModel { + let fetchRequest = DeviceHardwareEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "hwModel == %d", userHwModel) + let fetchedHardware = try? context.fetch(fetchRequest) + if let hardwareEntity = fetchedHardware?.first, let archString = hardwareEntity.architecture, let arch = Architecture(rawValue: archString) { + architecture = arch } } + // Need to request a NetworkConfig from the remote node before allowing changes if let deviceNum = accessoryManager.activeDeviceNum, let node { let connectedNode = getNodeInfo(id: deviceNum, context: context) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift deleted file mode 100644 index 490f7e01..00000000 --- a/Meshtastic/Views/Settings/Firmware.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// Firmware.swift -// Meshtastic -// -// Copyright(c) by Garth Vander Houwen on 3/10/23. -// - -import SwiftUI -import StoreKit -import OSLog - -struct Firmware: View { - @Environment(\.managedObjectContext) var context - @EnvironmentObject var accessoryManager: AccessoryManager - var node: NodeInfoEntity? - @State var minimumVersion = "2.5.4" - @State var version = "" - @State private var currentDevice: DeviceHardware? - @State private var latestStable: FirmwareRelease? - @State private var latestAlpha: FirmwareRelease? - - var body: some View { - let supportedVersion = accessoryManager.checkIsVersionSupported(forVersion: minimumVersion) - let connectedVersion = accessoryManager.activeConnection?.device.firmwareVersion ?? "Unknown" - ScrollView { - VStack(alignment: .leading) { - let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "") - - HStack { - VStack { - Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle") - .font(.largeTitle) - .foregroundStyle(currentDevice?.activelySupported ?? false ? .green : .red) - Text( currentDevice?.activelySupported ?? false ? "Supported" : "Unsupported") - .foregroundStyle(.gray) - .font(.caption2) - } - Text("Device Model: \(currentDevice?.displayName ?? "Unknown")") - .font(.largeTitle) - .fixedSize(horizontal: false, vertical: true) - } - VStack { - Image(deviceString ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 300, height: 300) - .cornerRadius(5) - } - - if supportedVersion { - Text("Your Firmware is up to date") - .fixedSize(horizontal: false, vertical: true) - .foregroundStyle(.green) - .font(.title2) - .padding(.bottom) - Text("Current Firmware Version: \(connectedVersion)") - .fixedSize(horizontal: false, vertical: true) - .font(.title3) - .padding(.bottom) - } else { - Text("Newer firmware is available") - .fixedSize(horizontal: false, vertical: true) - .foregroundStyle(.red) - .font(.title2) - .padding(.bottom) - Text("Current Firmware Version: \(connectedVersion), Latest Firmware Version: \(minimumVersion)") - .fixedSize(horizontal: false, vertical: true) - .font(.title3) - .padding(.bottom) - } - Divider() - Text("How to update Firmware") - .fixedSize(horizontal: false, vertical: true) - .font(.title2) - .padding(.bottom) - - Text("Get the latest stable firmware") - .fixedSize(horizontal: false, vertical: true) - .font(.callout) - Link("\(latestStable?.title ?? "Unknown".localized)", destination: URL(string: "\(latestStable?.zipURL ?? "https://meshtastic.org")")!) - .font(.caption) - Link("Release Notes", destination: URL(string: "\(latestStable?.pageURL ?? "https://meshtastic.org")")!) - .font(.caption) - .padding(.bottom) - - if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { - VStack(alignment: .leading) { - - Text("Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor.") - .fixedSize(horizontal: false, vertical: true) - .foregroundStyle(.gray) - .font(.caption) - Link("Drag & Drop Firmware Update Documentation", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) - .font(.caption) - .padding(.bottom) - VStack { - Text("If it is hard to access your device's reset button enter DFU mode here.") - .fixedSize(horizontal: false, vertical: true) - .foregroundStyle(.gray) - .font(.caption) - Button { - let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context) - if connectedNode != nil { - Task { - do { - try await accessoryManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) - Task { - try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 1 second - try await accessoryManager.disconnect() - } - } catch { - Logger.mesh.error("Enter DFU Failed") - } - } - } - } label: { - Label("Enter DFU Mode", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(5) - } - Spacer() - /// RAK 4631 - if currentDevice?.hwModel == 9 { - Text("You can also update your Meshtastic device over bluetooth using the Nordic DFU app.") - .fixedSize(horizontal: false, vertical: true) - .foregroundStyle(.gray) - .font(.caption) - Link("Get NRF DFU from the App Store", destination: URL(string: "https://apps.apple.com/us/app/nrf-device-firmware-update/id1624454660")!) - .font(.callout) - .padding(.bottom) - } else { - Text("OTA Updates are not supported on this NRF Device.") - .font(.title3) - Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) - .font(.callout) - } - } - } else if currentDevice?.architecture == Meshtastic.Architecture.esp32 || currentDevice?.architecture == Meshtastic.Architecture.esp32S3 || currentDevice?.architecture == Meshtastic.Architecture.esp32C3 { - VStack(alignment: .leading) { - Text("ESP32 Device Firmware Update") - .font(.title3) - Text("Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE.") - .font(.caption) - Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!) - .font(.callout) - .padding(.bottom) - Text("ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message.") - .font(.caption) - HStack(alignment: .center) { - Spacer() - Button { - let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context) - if connectedNode != nil { - Task { - do { - try await accessoryManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!) - } catch { - Logger.mesh.error("Reboot Failed") - } - } - } - } label: { - Label("Send Reboot OTA", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(5) - Spacer() - } - } - } else { - Text("OTA Updates are not supported on your platform.") - .font(.title3) - Text(node?.user?.hwModel ?? "UNSET") - .font(.title3) - Text( currentDevice?.architecture.rawValue ?? "UNKNOWN") - .font(.title3) - } - } - .padding() - .padding(.bottom, 5) - .onFirstAppear { - Api().loadDeviceHardwareData { (hw) in - for device in hw { - let currentHardware = node?.user?.hwModel ?? "UNSET" - let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { - currentDevice = device - } - } - } - Api().loadFirmwareReleaseData { (fw) in - latestStable = fw.releases.stable.first - let archString = currentDevice?.architecture.rawValue ?? "" - let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) - latestStable = fw.releases.stable.first - latestAlpha = fw.releases.alpha.first - } - } - .navigationTitle("Firmware Updates") - .navigationBarTitleDisplayMode(.inline) - } - } -} diff --git a/Meshtastic/Views/Settings/Firmware/ESP32 DFU/ESP32DFUSheet.swift b/Meshtastic/Views/Settings/Firmware/ESP32 DFU/ESP32DFUSheet.swift new file mode 100644 index 00000000..6ba00056 --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware/ESP32 DFU/ESP32DFUSheet.swift @@ -0,0 +1,65 @@ +// +// ESP32DFUSheet.swift +// Meshtastic +// +// Created by Jake Bordens on 12/12/25. +// + +import SwiftUI +import OSLog + +struct ESP32DFUSheet: View { + @EnvironmentObject var accessoryManager: AccessoryManager + @Environment(\.dismiss) var dismiss + @Environment(\.managedObjectContext) var context + + var body: some View { + NavigationView { // Use a NavigationView for a title bar + VStack(alignment: .leading, spacing: 20.0) { + Text("ESP32 Device Firmware Update") + .font(.title) + Text("Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE.") + .font(.body) + Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!) + .font(.body) + .padding() + Text("ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message.") + .font(.body) + HStack(alignment: .center) { + Spacer() + Button { + let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context) + if let connectedNode, let user = connectedNode.user { + Task { + do { + try await accessoryManager.sendRebootOta(fromUser: user, toUser: user) + } catch { + Logger.mesh.error("Reboot Failed") + } + } + } + } label: { + Label("Send Reboot OTA", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(5) + Spacer() + } + }.padding(20.0) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + // 2. Create a button that calls dismiss() + Button("Done") { + dismiss() + } + } + } + } + } +} + +#Preview { + ESP32DFUSheet() +} diff --git a/Meshtastic/Views/Settings/Firmware/Firmware.swift b/Meshtastic/Views/Settings/Firmware/Firmware.swift new file mode 100644 index 00000000..856d968f --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware/Firmware.swift @@ -0,0 +1,294 @@ +// +// Firmware.swift +// Meshtastic +// +// Copyright(c) by Garth Vander Houwen on 3/10/23. +// + +import SwiftUI +import StoreKit +import OSLog + +struct Firmware: View { + + private enum FirmwareTab { + case stable + case alpha + case downloaded + } + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var accessoryManager: AccessoryManager + let node: NodeInfoEntity + let hardware: DeviceHardwareEntity + @State var minimumVersion = "2.6.11" + @State var version = "" + @State private var currentDevice: DeviceHardware? + + @State private var firmwareSelection = FirmwareTab.stable + + @EnvironmentObject var meshtasticAPI: MeshtasticAPI + + @StateObject var firmwareList: FirmwareViewModel + + init?(node: NodeInfoEntity?) { + guard let node else { return nil } + self.node = node + + let fetchRequest = DeviceHardwareEntity.fetchRequest() + guard let pioEnv = node.myInfo?.pioEnv else { return nil } + fetchRequest.predicate = NSPredicate(format: "platformioTarget == %@", pioEnv) + fetchRequest.fetchLimit = 1 + + // Can't use the @Environment because we don't have self yet. + let context = PersistenceController.shared.container.viewContext + guard let result = try? context.fetch(fetchRequest).first else { + return nil + } + hardware = result + _firmwareList = StateObject(wrappedValue: FirmwareViewModel(forHardware: result)) + } + + var myVersion: String? { + return node.metadata?.firmwareVersion + } + + @ViewBuilder + var firmwareLastUpdatedFooter: some View { + HStack(alignment: .firstTextBaseline, spacing: 0.0) { + if self.meshtasticAPI.isLoadingFirmwareList { + Text("Updating now...") + } else { + if UserDefaults.lastFirmwareAPIUpdate == .distantPast { + Text("Last Updated: Never") + } else { + Text("Last Updated: \(UserDefaults.lastFirmwareAPIUpdate.formatted(date: .numeric, time: .shortened))") + } + } + } + } + + @ViewBuilder + var fimwareReleasesHeader: some View { + HStack { + Text("Firmware Releases") + Spacer() + if meshtasticAPI.isLoadingFirmwareList { + ProgressView() + } else { + Button { + Task.detached { + try? await meshtasticAPI.refreshFirmwareAPIData() + } + } label: { + Text("Check For Updates") + } + } + } + } + + @StateObject private var dfuViewModel = DFUViewModel() + + var body: some View { + List { + // Hero image of the node + Section { + HStack { + SupportedHardwareBadge(hwModelId: hardware.hwModel) + Text("Device Model: \(hardware.displayName ?? "Unknown")") + .font(.largeTitle) + .fixedSize(horizontal: false, vertical: true) + } + VStack(alignment: .center) { + DeviceHardwareImage(hwId: node.user?.hwModelId ?? 0) + .frame(width: 300, height: 300) + .cornerRadius(5) + }.frame(maxWidth: .infinity) // Make sure the center is honored by filling the width + VStack(alignment: .leading) { + Text("Platform IO").font(.caption).foregroundColor(.secondary) + Text("\(node.myInfo?.pioEnv, default: "Unknown")") + } + VStack(alignment: .leading) { + Text("Architecture").font(.caption).foregroundColor(.secondary) + Text("\(self.hardware.architecture, default: "Unknown")") + } + VStack(alignment: .leading) { + Text("Current Firmware Version").font(.caption).foregroundColor(.secondary) + Text("\(self.myVersion, default: "Unknown")") + } + }.listRowSeparator(.hidden) // Hides lines between rows + + Section(header: self.fimwareReleasesHeader, footer: self.firmwareLastUpdatedFooter) { + Picker("Firmware Version", selection: $firmwareSelection) { + Text("Stable").tag(FirmwareTab.stable) + Text("Alpha").tag(FirmwareTab.alpha) + Text("Downloaded").tag(FirmwareTab.downloaded) + }.pickerStyle(.segmented) + + switch firmwareSelection { + case .stable: + let stables = firmwareList.mostRecentFirmware(forReleaseType: .stable) + ForEach(stables, id: \.localUrl) { release in + FirmwareRow(firmwareFile: release) + } + if let lastStable = stables.last, let notes = lastStable.releaseNotes { + NavigationLink { + ScrollView { + Text(notes) + .padding() + }.navigationTitle("\(lastStable.versionId, default: "ReleaseNotes")") + } label: { + Text("Release Notes") + } + } + case .alpha: + let alphas = firmwareList.mostRecentFirmware(forReleaseType: .alpha) + ForEach(alphas, id: \.localUrl) { release in + FirmwareRow(firmwareFile: release) + } + if let lastAlpha = alphas.last, let notes = lastAlpha.releaseNotes { + NavigationLink { + ScrollView { + Text(notes) + .padding() + }.navigationTitle("\(lastAlpha.versionId, default: "ReleaseNotes")") + } label: { + Text("Release Notes") + } + } + case .downloaded: + let downloadedFirmware = firmwareList.downloadedFirmware(includeInProgressDownloads: true) + if downloadedFirmware.count > 0 { + ForEach(downloadedFirmware, id: \.localUrl) { firmwareFile in + FirmwareRow(firmwareFile: firmwareFile) + }.onDelete { offsets in + let filesToDelete = offsets.map { downloadedFirmware[$0] } + firmwareList.delete(filesToDelete) + } + } else { + Text("No firmware has been downloaded for this device.") + } + } + } + .navigationTitle("Firmware Updates") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +struct FirmwareTagView: View { + let text: String + let color: Color + init(_ text: String, color: Color = .black) { + self.text = text + self.color = color + } + var body: some View { + Text(text) + .foregroundStyle(color) + .padding(.horizontal, 4.0) + .padding(.vertical, 2.0) + .font(.footnote) + .background(RoundedRectangle(cornerRadius: 4.0).stroke(color, lineWidth: 1.5)) + + } +} + +private struct FirmwareRow: View { + + @ObservedObject var firmwareFile: FirmwareFile + + @State var unsupporedInstallationMessage: Bool = false + @State var showInstallationSheet: FirmwareFile.FirmwareType? + + var body: some View { + VStack { + HStack { + switch firmwareFile.firmwareType { + case .uf2: + Text("UF2").font(.caption2) + case .bin: + Text("BIN").font(.caption2) + case .otaZip: + Text("ZIP").font(.caption2) + } + + Text("\(firmwareFile.versionId)") + + switch firmwareFile.releaseType { + case .stable: + FirmwareTagView("STABLE", color: Color.green) + case .alpha: + FirmwareTagView("ALPHA", color: Color.blue) + case .unlisted: + FirmwareTagView("UNLISTED", color: Color.orange) + } + + Spacer() + + switch firmwareFile.status { + case .downloading: + ProgressView() + + case .downloaded: + Button { + switch firmwareFile.firmwareType { + case .uf2: + self.showInstallationSheet = .uf2 + case .bin: + self.unsupporedInstallationMessage = true + case .otaZip: + self.showInstallationSheet = .otaZip + } + } label: { + HStack(alignment: .firstTextBaseline, spacing: 2.0) { + Text("Install") + self.installIcon + } + }.buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(2.0) + + case .notDownloaded: + Button { + Task { + try? await firmwareFile.download() + } + } label: { + Text("Download") + }.buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(2.0) + case .error: + Image(systemName: "exclamationmark.triangle.fill").foregroundColor(.red) + } + } + }.alert(isPresented: $unsupporedInstallationMessage) { + Alert(title: Text("Unsupported Installation"), + message: Text("Firmware installation is not supported for this device architecture."), + dismissButton: .default(Text("OK"))) + }.sheet(item: $showInstallationSheet) { type in + switch type { + case .otaZip: + NRFDFUSheet(firmwareToFlash: firmwareFile.localUrl) + case .uf2: + UF2MassStorageView(fileURL: firmwareFile.localUrl) + case .bin: + ESP32DFUSheet() + } + } + } + + private var installIcon: Image? { + switch firmwareFile.firmwareType { + case .uf2: + return Image("custom.usb") + case .bin: + return nil + case .otaZip: + return Image("custom.bluetooth") + } + } +} diff --git a/Meshtastic/Views/Settings/Firmware/NRF DFU/DFUModel.swift b/Meshtastic/Views/Settings/Firmware/NRF DFU/DFUModel.swift new file mode 100644 index 00000000..649115f5 --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware/NRF DFU/DFUModel.swift @@ -0,0 +1,132 @@ +// +// to.swift +// Meshtastic +// +// Created by jake on 12/2/25. +// + + +import Foundation +import NordicDFU +import CoreBluetooth +import OSLog +import UIKit + +// A simple enum to track the UI state +enum DFUUpdateState: Equatable { + case idle + case starting + case uploading + case success + case error(String) +} + +class DFUViewModel: NSObject, ObservableObject { + // MARK: - Published Properties (UI Binding) + @Published var progress: Double = 0.0 + @Published var state: DFUUpdateState = .idle + @Published var statusMessage: String = "Ready" + @Published var rotatingMessage: String = "" + + var lastRotatingMessageUpdate = Date.distantPast + var rotatingMessageIndex = -1 + let rotatingMessages = ["Hang tight, Garth is working on it.", "Keep your device close to the phone or Ben will be angry.", "Dan says, \"Do not close the app!\""] + + // MARK: - DFU Controller + private var dfuController: DFUServiceController? + + // MARK: - Start DFU + /// Call this function from your SwiftUI View + /// - Parameters: + /// - peripheral: The CoreBluetooth device you are connected to + /// - zipFileUrl: The local URL of the Firmware Zip file + func startDFU(peripheral: CBPeripheral, zipFileUrl: URL) { + + guard let firmware = try? DFUFirmware(urlToZipFile: zipFileUrl) else { + self.state = .error("Invalid Zip File") + return + } + + // Setup the initiator + let initiator = DFUServiceInitiator(queue: .main, delegateQueue: .main) + + initiator.forceScanningForNewAddressInLegacyDfu = true + initiator.dataObjectPreparationDelay = 0.4 + initiator.enableUnsafeExperimentalButtonlessServiceInSecureDfu = true + initiator.forceDfu = false + initiator.disableResume = true + initiator.packetReceiptNotificationParameter = 8 + + // Set self as delegate + initiator.delegate = self + initiator.progressDelegate = self + initiator.logger = self // Optional: For debugging + + // Start the process + self.state = .uploading + self.dfuController = initiator.with(firmware: firmware) + .start(target: peripheral) + } + + // Abort function + func abort() { + _ = dfuController?.abort() + } +} + +// MARK: - DFU Service Delegate (State Changes) +extension DFUViewModel: DFUServiceDelegate { + + func dfuStateDidChange(to state: DFUState) { + // Map Nordic's internal state to our UI string + switch state { + case .starting: + UIApplication.shared.isIdleTimerDisabled = true + self.rotatingMessage = "This can take a while. Please be patient." + self.state = .starting + case .completed: + UIApplication.shared.isIdleTimerDisabled = false + self.state = .success + self.statusMessage = "Update Complete" + self.rotatingMessage = "Firmware Update Successful!" + self.progress = 1.0 + case .disconnecting: + UIApplication.shared.isIdleTimerDisabled = false + self.statusMessage = "Disconnecting..." + case .aborted: + UIApplication.shared.isIdleTimerDisabled = false + self.state = .error("Aborted") + self.statusMessage = "Update Aborted" + default: + self.statusMessage = state.description + } + Logger.services.info("NRF DFU State changed: \(state.description)") + } + + func dfuError(_ error: DFUError, didOccurWithMessage message: String) { + self.state = .error(message) + self.statusMessage = "Error: \(message)" + } +} + +// MARK: - DFU Progress Delegate (Progress Bar) +extension DFUViewModel: DFUProgressDelegate { + func dfuProgressDidChange(for part: Int, outOf totalParts: Int, to progress: Int, currentSpeedBytesPerSecond: Double, avgSpeedBytesPerSecond: Double) { + // Convert 0-100 Int to 0.0-1.0 Double for SwiftUI ProgressView + self.progress = Double(progress) / 100.0 + + if lastRotatingMessageUpdate.timeIntervalSinceNow < -10 { + // Last message was 10 seconds ago. This insures messages don't rotate too fast + lastRotatingMessageUpdate = Date() + self.rotatingMessageIndex = (self.rotatingMessageIndex + 1) % self.rotatingMessages.count + self.rotatingMessage = self.rotatingMessages[self.rotatingMessageIndex] + } + } +} + +// MARK: - Logger Delegate (Optional) +extension DFUViewModel: LoggerDelegate { + func logWith(_ level: LogLevel, message: String) { + Logger.services.info("NRF DFU Log: \(message)") + } +} diff --git a/Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift b/Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift new file mode 100644 index 00000000..841b6190 --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift @@ -0,0 +1,151 @@ +// +// NRFDFUSheet.swift +// Meshtastic +// +// Created by Jake Bordens on 12/11/25. +// + +import SwiftUI + +struct NRFDFUSheet: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var accessoryManager: AccessoryManager + @State var showWarningAlert = true + @StateObject private var dfuViewModel = DFUViewModel() + + let firmwareToFlash: URL + + let alertMessage = """ + You are about to flash new firmware to your device. This process carries risks. Unsucessful updates may brick the device and require re-flashing the bootloader. + + * Ensure your device is charged. + * Connect your device to a stable power supply. + * Keep the device close to your phone. + * Do not close the app during the update. + * Verify you have selected the correct firmware for your hardware. + + Note: This will temporarily a disconnect your device during the update. + """ + + var body: some View { + NavigationView { // Use a NavigationView for a title bar + VStack(spacing: 20.0) { + Text("DFU Firmware Update") + .font(.headline) + + Text("Please do not leave this screen until this process is complete.") + .multilineTextAlignment(.center) + .padding() + + switch dfuViewModel.state { + case .idle: + Button("Begin Update") { + Task { + // Action for your primary button + if let connection = accessoryManager.activeConnection?.connection as? BLEConnection { + let peripheral = await connection.peripheral + dfuViewModel.startDFU(peripheral: peripheral, zipFileUrl: firmwareToFlash) + } + } + } + .buttonStyle(.borderedProminent) + .disabled(showWarningAlert) // Make sure it can't be tapped till the warning is dismissed. + + case .uploading, .starting, .success: + VStack(spacing: 20.0) { + CircularProgressView(progress: dfuViewModel.progress, size: 225.0, subtitleText: dfuViewModel.statusMessage) + Text(dfuViewModel.rotatingMessage) + .multilineTextAlignment(.center) + + }.frame(maxHeight: .infinity) + case .error(let message): + Text("Error: \(message)") + } + }.toolbar { + ToolbarItem(placement: .navigationBarLeading) { + // 2. Create a button that calls dismiss() + Button("Done") { + dismiss() + }.disabled([.starting, .uploading].contains(dfuViewModel.state)) + } + } + }.alert("Update Warning", isPresented: $showWarningAlert) { + // Add buttons here + Button("I Know What I'm Doing", role: .destructive) { } + Button("Not Now", role: .cancel) { + dismiss() + } + } message: { + Text(alertMessage) + } + .navigationTitle("Nordic DFU Update") + .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled(true) + } +} + +private struct CircularProgressView: View { + let progress: Double + var lineWidth: CGFloat = 20 + var size: CGFloat = 150 + var strokeColor: Color = .blue + var backgroundColor: Color = .gray.opacity(0.2) + var percentageFontSize: CGFloat = 48.0 + var subtitleText: String = "Complete" + var showSubtitle: Bool = true + + private var isComplete: Bool { + progress >= 1.0 + } + + var body: some View { + ZStack { + // Background circle + Circle() + .stroke(backgroundColor, lineWidth: lineWidth) + + // Progress circle + Circle() + .trim(from: 0, to: progress) + .stroke(isComplete ? .green : strokeColor, style: StrokeStyle( + lineWidth: lineWidth, + lineCap: .round + )) + .rotationEffect(.degrees(-90)) + .animation(.spring(response: 0.6), value: progress) + + // Content + if isComplete { + ZStack { + // Optional: filled circle background + Circle() + .fill(Color.green.opacity(0.15)) + .frame(width: size * 0.6, height: size * 0.6) + + // Checkmark + Image(systemName: "checkmark.circle.fill") + .font(.system(size: percentageFontSize * 1.5, weight: .bold)) + .foregroundColor(.green) + } + .transition(.scale.combined(with: .opacity)) + } else { + VStack(spacing: 8) { + Text("\(Int(progress * 100))%") + .font(.system(size: percentageFontSize, weight: .bold)) + .foregroundColor(.primary) + .contentTransition(.numericText()) + .animation(.default, value: progress) + + if showSubtitle { + Text(subtitleText) + .font(.callout) + .foregroundColor(.secondary) + } + } + .transition(.scale.combined(with: .opacity)) + } + } + .frame(width: size, height: size) + .animation(.spring(response: 0.5, dampingFraction: 0.7), value: isComplete) + } +} diff --git a/Meshtastic/Views/Settings/Firmware/U2F Mass Storage/UF2MassStorageView.swift b/Meshtastic/Views/Settings/Firmware/U2F Mass Storage/UF2MassStorageView.swift new file mode 100644 index 00000000..eebc69a6 --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware/U2F Mass Storage/UF2MassStorageView.swift @@ -0,0 +1,148 @@ +// +// UF2MassStorageView.swift +// Meshtastic +// +// Created by jake on 12/12/25. +// + +import SwiftUI +import OSLog + +struct UF2MassStorageView: View { + @EnvironmentObject var accessoryManager: AccessoryManager + @Environment(\.dismiss) var dismiss + @Environment(\.managedObjectContext) var context + + @State private var isExporting = false + @State private var document: FirmwareDocument? + + let fileURL: URL + var body: some View { + NavigationView { // Use a NavigationView for a title bar + ScrollView { + VStack(spacing: 20) { + + HStack(alignment: .top, spacing: 12) { + Image(systemName: "lock.shield") + .font(.title2) + .foregroundStyle(.blue) + + Text("For security reasons, iOS cannot write directly to external USB devices. You must save the file manually.") + .font(.callout) + } + .padding() + .background(Color.secondary.opacity(0.1)) + .cornerRadius(12) + + Divider() + + VStack(alignment: .leading, spacing: 12) { + Label("Step 1: Connect Device", systemImage: "1.circle.fill") + .font(.headline) + + Text("Place your device in DFU mode and connect it via USB.") + .fixedSize(horizontal: false, vertical: true) + + Text("If connected, use the button below to reboot into DFU. Otherwise, press your device's reset button twice rapidly.") + .font(.caption) + .foregroundStyle(.secondary) + + resetIntoOTAButton() // Ensure this button has suitable padding/styling + } + + Divider() + + VStack(alignment: .leading, spacing: 12) { + Label("Step 2: Save the File", systemImage: "2.circle.fill") + .font(.headline) + + VStack(alignment: .leading, spacing: 8) { + Text("• Tap the **Save Firmware to USB** button below.") + Text("• Navigate all the way back to **Locations** in the file picker.") + Text("• Select your USB device and tap **Save**.") + } + .font(.callout) + + exportFirmwareButton() // Ensure this button is prominent + } + + Divider() + + VStack(alignment: .leading, spacing: 10) { + Label("Important Notes", systemImage: "info.circle") + .font(.caption.bold()) + .foregroundStyle(.secondary) + + Text("• The filename will be a random string ending in `.uf2` to prevent iOS caching.") + Text("• You may see an error saying the file could not be saved. This is normal, as the device disconnects immediately after updating.") + } + .font(.caption) + .foregroundStyle(.secondary) + }.padding() + }.navigationTitle("UF2 Firmware Update") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + // 2. Create a button that calls dismiss() + Button("Done") { + dismiss() + } + } + } + }.fileExporter( + isPresented: $isExporting, + document: document, + contentType: .UF2Firmware, // Use your custom type here + defaultFilename: UUID().uuidString // No extension needed here, UTType handles it + ) { result in + switch result { + case .success(let url): + Logger.services.info("Firmware Saved to \(url.path)") + case .failure(let error): + Logger.services.error("Failed to save firmware: \(error.localizedDescription)") + } + } + } + + @ViewBuilder + func resetIntoOTAButton() -> some View { + Button { + let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context) + if let connectedNode, let user = connectedNode.user { + Task { + do { + try await accessoryManager.sendEnterDfuMode(fromUser: user, toUser: user) + } catch { + Logger.mesh.error("Reboot Failed") + } + } + } + } label: { + Label(" Send Reboot into DFU", systemImage: "square.and.arrow.down") + }.buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(maxWidth: .infinity) + .cornerRadius(10).disabled(accessoryManager.activeDeviceNum == nil) + } + + @ViewBuilder + func exportFirmwareButton() -> some View { + Button(action: { + prepareFirmwareForExport() + }) { + Label("Save Firmware to USB", systemImage: "externaldrive.fill") + }.buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(maxWidth: .infinity) + } + + func prepareFirmwareForExport() { + if let data = try? Data(contentsOf: fileURL) { + // 2. Initialize the document + self.document = FirmwareDocument(data: data) + + // 3. Trigger the sheet + self.isExporting = true + } + } +} diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift deleted file mode 100644 index a72be24c..00000000 --- a/Meshtastic/Views/Settings/FirmwareApi.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// FirmwareApi.swift -// Meshtastic -// -// Created by Garth Vander Houwen on 12/27/23. -// - -import Foundation -import OSLog - -/// Device Hardware API -struct DeviceHardware: Codable { - let hwModel: Int - let hwModelSlug: String - let platformioTarget: String - let architecture: Architecture - let activelySupported: Bool - let displayName: String - let supportLevel: Int? - let tags: [String]? - let images: [String]? - let requiresDfu: Bool? - let hasInkHud: Bool? - let partitionScheme: String? - let hasMui: Bool? -} -enum Architecture: String, Codable { - case esp32 = "esp32" - case esp32C3 = "esp32-c3" - case esp32S3 = "esp32-s3" - case nrf52840 = "nrf52840" - case rp2040 = "rp2040" - case esp32C6 = "esp32-c6" -} - -/// Firmware Release Lists -struct FirmwareReleases: Codable { - let releases: Releases - let pullRequests: [FirmwareRelease] -} -struct Releases: Codable { - let stable, alpha: [FirmwareRelease] -} -struct FirmwareRelease: Codable { - let id, title: String - let pageURL: String - let zipURL: String - - enum CodingKeys: String, CodingKey { - case id, title - case pageURL = "page_url" - case zipURL = "zip_url" - } -} - -class Api: ObservableObject { - - func loadDeviceHardwareData(completion: @escaping ([DeviceHardware]) -> Void) { - - /// List from https://api.meshtastic.org/resource/deviceHardware - guard let url = Bundle.main.url(forResource: "DeviceHardware.json", withExtension: nil) else { - Logger.services.critical("Couldn't find DeviceHardware.json in main bundle.") - return - } - - URLSession.shared.dataTask(with: url) { data, _, _ in - if let data = data { - do { - let deviceHardware = try JSONDecoder().decode([DeviceHardware].self, from: data) - DispatchQueue.main.async { - completion(deviceHardware) - } - } catch { - Logger.services.error("JSON decode failure: \(error.localizedDescription, privacy: .public)") - if let decodingError = error as? DecodingError { - Logger.services.error("Decoding error details: \(decodingError)") - } - } - return - } - }.resume() - } - - func loadFirmwareReleaseData(completion: @escaping (FirmwareReleases) -> Void) { - guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else { - Logger.services.error("Invalid url...") - return - } - URLSession.shared.dataTask(with: url) { data, _, _ in - if let data = data { - do { - let firmwareReleases = try JSONDecoder().decode(FirmwareReleases.self, from: data) - DispatchQueue.main.async { - completion(firmwareReleases) - } - } catch { - Logger.services.error("JSON decode failure: \(error.localizedDescription, privacy: .public)") - } - return - } - }.resume() - } -} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index d3d15a66..966daf87 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -9,6 +9,7 @@ import SwiftUI import OSLog import TipKit import MeshtasticProtobufs +import CoreData struct Settings: View { @Environment(\.managedObjectContext) var context @@ -311,6 +312,77 @@ struct Settings: View { Image(systemName: "folder") } } + NavigationLink { + CoreDataBrowser() + } label: { + Label("Database Browser", systemImage: "server.rack") + } + Button { + Task.detached { + await MainActor.run { + let persistenceController = PersistenceController.shared.container + for i in 0...persistenceController.managedObjectModel.entities.count-1 { + + let entity = persistenceController.managedObjectModel.entities[i] + let query = NSFetchRequest(entityName: entity.name!) + var deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + let entityName = entity.name ?? "UNK" + + if !["DeviceHardwareEntity","DeviceHardwareImageEntity", "DeviceHardwareTagEntity"].contains(entityName) { + // These are non-node-specific "app level" data, keep them even when switching nodes + continue + } + + // Execute the delete for this entry + deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + do { + try context.executeAndMergeChanges(using: deleteRequest) + } catch { + Logger.data.error("\(error.localizedDescription, privacy: .public)") + } + } + } + Logger.data.debug("CoreData Delete complete, waiting 3 seconds") + try? await Thread.sleep(forTimeInterval: 5.0) + Logger.data.debug("Refreshing from API") + try? await MeshtasticAPI.shared.refreshDevicesAPIData() + } + } label: { + Text("Test Devices API Refresh") + } + Button { + Task.detached { + await MainActor.run { + let persistenceController = PersistenceController.shared.container + for i in 0...persistenceController.managedObjectModel.entities.count-1 { + + let entity = persistenceController.managedObjectModel.entities[i] + let query = NSFetchRequest(entityName: entity.name!) + var deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + let entityName = entity.name ?? "UNK" + + if !["FirmwareReleaseEntity"].contains(entityName) { + // These are non-node-specific "app level" data, keep them even when switching nodes + continue + } + + // Execute the delete for this entry + deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + do { + try context.executeAndMergeChanges(using: deleteRequest) + } catch { + Logger.data.error("\(error.localizedDescription, privacy: .public)") + } + } + } + Logger.data.debug("CoreData Delete complete, waiting 3 seconds") + try? await Thread.sleep(forTimeInterval: 5.0) + Logger.data.debug("Refreshing Firmware from API") + try? await MeshtasticAPI.shared.refreshFirmwareAPIData() + } + } label: { + Text("Test Firmware API Refresh") + } } } @@ -520,7 +592,11 @@ struct Settings: View { case .appFiles: AppData() case .firmwareUpdates: - Firmware(node: node) + if let firmwareView = Firmware(node: node) { + firmwareView + } else { + Text("Please connect to a device to see firmware updates.") + } } } .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in diff --git a/Settings.bundle/Acknowledgments.plist b/Settings.bundle/Acknowledgments.plist new file mode 100755 index 00000000..3621e58b --- /dev/null +++ b/Settings.bundle/Acknowledgments.plist @@ -0,0 +1,212 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + FooterText + Portions of this application may use the following copyrighted material which is acknowledged below: + + + Type + PSGroupSpecifier + FooterText + This project is dual licensed under the Eclipse Public License 1.0 and the Eclipse Distribution License 1.0 as described in the epl-v10 and edl-v10 files. + Title + CocoaMQTT + + + Type + PSGroupSpecifier + FooterText + Copyright 2017 Datadog, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Title + Datadog SDK for iOS + + + Type + PSGroupSpecifier + FooterText + This library is in the public domain. + Title + MqttCocoaAsyncSocket + + + Type + PSGroupSpecifier + FooterText + BSD 3-Clause License + +Copyright (c) 2019-2024, Nordic Semiconductor +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + Title + Nordic iOS DFU Library + + + Type + PSGroupSpecifier + FooterText + Copyright 2017 Datadog, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Title + OpenTelemetry Swift Packages + + + Type + PSGroupSpecifier + FooterText + Except as noted below, PLCrashReporter is provided under the +following license: + +Copyright (c) Microsoft Corporation. +Copyright (c) 2008 - 2014 Plausible Labs Cooperative, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Additional contributions have been made under the same license terms as above, with copyright held by their respective authors: + +Damian Morris - damian@moso.com.au +Copyright (c) 2010 MOSO Corporation, Pty Ltd. +All rights reserved. + +HockeyApp/Bitstadium +Copyright (c) 2012 HockeyApp, Bit Stadium GmbH. +All rights reserved. + +The protobuf-c library, as well as the PLCrashLogWriterEncoding.c file are licensed as follows: + +Copyright 2008, Dave Benson. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Title + PLCrashReporter + + + Type + PSGroupSpecifier + FooterText + Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +Copyright (c) 2014-2023 Dalton Cherry. + + Title + Starscream + + + Type + PSGroupSpecifier + FooterText + Copyright (c) 2019 Simon Whitty + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + Title + SwiftDraw + + + Type + PSGroupSpecifier + FooterText + Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +Copyright (c) 2014 - 2016 Apple Inc. and the project authors + + Title + SwiftDraw + + + Type + PSGroupSpecifier + FooterText + MIT License + +Copyright (c) 2017-2025 Thomas Zoechling (https://www.peakstep.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + ZIPFoundation + + + + diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index 681fc817..a20077e6 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -104,6 +104,20 @@ DefaultValue + + Type + PSGroupSpecifier + Title + Acknowledgments + + + Type + PSChildPaneSpecifier + Title + Acknowledgments + File + Acknowledgments + diff --git a/scripts/download_images.py b/scripts/download_images.py new file mode 100755 index 00000000..56bce113 --- /dev/null +++ b/scripts/download_images.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import shutil +import argparse +import socket +import urllib.request +import urllib.error +from concurrent.futures import ThreadPoolExecutor, as_completed +import threading +import hashlib + +# --- CONFIGURATION --- +API_URL = "https://api.meshtastic.org/resource/deviceHardware" +IMAGE_BASE_URL = "https://flasher.meshtastic.org/img/devices/" +REQUEST_TIMEOUT = 15 +MAX_WORKERS = 16 +# --- END CONFIGURATION --- + +print_lock = threading.Lock() + +def locked_print(*args, **kwargs): + with print_lock: + print(*args, **kwargs) + +def get_contents_json(filename): + data = { + "images": [{"filename": filename, "idiom": "universal"}], + "info": {"author": "xcode", "version": 1} + } + if filename.lower().endswith('.svg'): + data["properties"] = {"preserves-vector-representation": True} + return data + +def load_manifest(manifest_path, log_warning): + if not os.path.exists(manifest_path): + return {} + try: + with open(manifest_path, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + log_warning(f"Could not read or parse manifest at '{manifest_path}'. Starting fresh.") + return {} + +def save_manifest(data, manifest_path): + with open(manifest_path, 'w') as f: + json.dump(data, f, indent=2) + +def download_image(url, local_path, log_warning): + """ + Downloads an image from a URL to a local path, but only if the content is valid. + Returns a tuple (success: bool, error_message: str|None). + """ + try: + with urllib.request.urlopen(url, timeout=REQUEST_TIMEOUT) as response: + if response.status != 200: + return (False, f"Server returned status {response.status} for {url}") + + content_type = response.headers.get('Content-Type', '').lower() + if not content_type.startswith('image/'): + return (False, f"Invalid content type '{content_type}' for image at {url}. Server may have returned an error page.") + + with open(local_path, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + return (True, None) + + except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout, IOError) as e: + return (False, f"Failed to download image from {url}: {e}") + +def process_image(image_filename, base_dir, local_manifest, verbose_print, log_warning, target_mode): + image_url = f"{IMAGE_BASE_URL}{image_filename}" + asset_name = os.path.splitext(image_filename)[0] + + if target_mode == "xcassets": + asset_dir = os.path.join(base_dir, f"{asset_name}.imageset") + local_image_path = os.path.join(asset_dir, image_filename) + else: + asset_dir = base_dir + local_image_path = os.path.join(base_dir, image_filename) + + verbose_print(f"Processing Asset: {asset_name} ({image_filename})") + + try: + request = urllib.request.Request(image_url, method='HEAD') + with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as head_response: + remote_etag = head_response.headers.get('ETag') or head_response.headers.get('Last-Modified') + except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout) as e: + log_warning(f"Could not check remote file status for {asset_name}: {e}") + return ("failed", image_filename, None) + + if not remote_etag: + log_warning(f"Could not get ETag/Last-Modified for {asset_name}. Forcing update.") + remote_etag = "force-update-" + str(os.urandom(8).hex()) + + local_info = local_manifest.get('files', {}).get(image_filename) + + should_download = False + status = "" + if not local_info or not os.path.exists(local_image_path): + verbose_print(f" -> New asset '{asset_name}'. Downloading...") + status = "new" + should_download = True + elif local_info.get("etag") != remote_etag: + verbose_print(f" -> ETag mismatch for '{asset_name}'. Updating...") + status = "updated" + should_download = True + else: + verbose_print(f" -> Asset '{asset_name}' is up-to-date. Skipping.") + return ("skipped", image_filename, remote_etag) + + if should_download: + os.makedirs(asset_dir, exist_ok=True) + success, error_message = download_image(image_url, local_image_path, log_warning) + if success: + if target_mode == "xcassets": + contents_json_path = os.path.join(asset_dir, "Contents.json") + with open(contents_json_path, 'w') as f: + json.dump(get_contents_json(image_filename), f, indent=2) + return (status, image_filename, remote_etag) + else: + log_warning(f"Download failed for {asset_name}: {error_message}") + if target_mode == "xcassets": + if os.path.exists(asset_dir): + shutil.rmtree(asset_dir) + else: + if os.path.exists(local_image_path): + os.remove(local_image_path) + return ("failed", image_filename, None) + + return ("skipped", image_filename, remote_etag) + +def main(): + parser = argparse.ArgumentParser(description="Downloads and syncs image assets.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--output-dir", + help="Path to a regular directory where images and the manifest will be stored." + ) + group.add_argument( + "--output-xcassets", + help="Path to the .xcassets directory to populate (legacy behavior)." + ) + parser.add_argument("--output-json", help="If the API data has changed, save the raw JSON to this filename.") + parser.add_argument("--force", action="store_true", help="Force a full sync, ignoring the API content hash check.") + parser.add_argument("--verbose", action="store_true", help="Enable detailed logging for debugging.") + args = parser.parse_args() + + target_mode = "xcassets" if args.output_xcassets else "directory" + base_dir = args.output_xcassets if target_mode == "xcassets" else args.output_dir + + def verbose_print(*p_args, **p_kwargs): + if args.verbose: + locked_print(*p_args, **p_kwargs) + + def log_warning(message): + locked_print(f"warning: {message}") + + def log_error(message): + locked_print(f"error: {message}", file=sys.stderr) + + manifest_file = os.path.join(base_dir, "image_manifest.json") + verbose_print(f"--- Starting Image Asset Sync ---") + verbose_print(f"Target Path: {base_dir} ({'xcassets' if target_mode == 'xcassets' else 'directory'} mode)") + os.makedirs(base_dir, exist_ok=True) + local_manifest = load_manifest(manifest_file, log_warning) + new_manifest = {} + + verbose_print(f"Fetching device list from {API_URL}...") + try: + with urllib.request.urlopen(API_URL, timeout=REQUEST_TIMEOUT) as response: + api_data_bytes = response.read() + new_api_hash = hashlib.sha256(api_data_bytes).hexdigest() + devices = json.loads(api_data_bytes.decode('utf-8')) + except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout) as e: + log_error(f"Could not fetch API data from {API_URL}: {e}") + sys.exit(0) # Fail silently, XCode may be building offline without a network + except json.JSONDecodeError: + log_error("Failed to parse JSON from API response. The server may be down or the response is corrupt.") + sys.exit(1) + + if not args.force: + previous_api_hash = local_manifest.get("api_hash") + if previous_api_hash == new_api_hash: + verbose_print("\nAPI data has not changed. Nothing to do. Use --force to override.") + verbose_print("--- Sync Complete ---") + sys.exit(0) + else: + verbose_print("API data has changed. Proceeding with sync.") + else: + verbose_print("Force flag detected. Skipping API hash check.") + + # --- SAVE JSON IF REQUESTED --- + if args.output_json: + verbose_print(f"Saving API JSON to '{args.output_json}'...") + try: + with open(args.output_json, 'w') as f: + json.dump(devices, f, indent=2) + except IOError as e: + log_error(f"Failed to save output JSON to {args.output_json}: {e}") + # ------------------------------ + + verbose_print(f"Found {len(devices)} devices in API response.") + required_image_filenames = set( + image_filename for device in devices for image_filename in device.get("images", []) + ) + stats = {"new": 0, "updated": 0, "skipped": 0, "failed": 0} + verbose_print(f"\n--- Syncing {len(required_image_filenames)} unique assets using up to {MAX_WORKERS} threads ---") + + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + future_to_image = { + executor.submit(process_image, filename, base_dir, local_manifest, verbose_print, log_warning, target_mode): filename + for filename in required_image_filenames + } + for future in as_completed(future_to_image): + image_filename = future_to_image[future] + try: + status, _, remote_etag = future.result() + stats[status] += 1 + if status != "failed": + if 'files' not in new_manifest: + new_manifest['files'] = {} + new_manifest['files'][image_filename] = {"etag": remote_etag} + else: + # Keep old entry if download failed to avoid re-download loop next time if API didn't change, + # but usually, we want to retry on failure. Here we just don't add it to new manifest, + # or we could copy the old one. Let's copy old info if available to be safe. + old_file_info = local_manifest.get('files', {}).get(image_filename) + if old_file_info: + if 'files' not in new_manifest: + new_manifest['files'] = {} + new_manifest['files'][image_filename] = old_file_info + except Exception as exc: + log_warning(f"An unexpected exception occurred while processing {image_filename}: {exc}") + stats["failed"] += 1 + + verbose_print("\n--- Pruning old assets ---") + pruned_count = 0 + for filename in list(local_manifest.get('files', {}).keys()): + if filename not in required_image_filenames: + asset_name = os.path.splitext(filename)[0] + if target_mode == "xcassets": + asset_dir = os.path.join(base_dir, f"{asset_name}.imageset") + verbose_print(f"Pruning {asset_name}...") + if os.path.exists(asset_dir): + shutil.rmtree(asset_dir) + else: + file_path = os.path.join(base_dir, filename) + verbose_print(f"Pruning {filename}...") + if os.path.exists(file_path): + os.remove(file_path) + pruned_count += 1 + if pruned_count == 0: + verbose_print("No assets to prune.") + + new_manifest['api_hash'] = new_api_hash + verbose_print("\nSaving new manifest...") + save_manifest(new_manifest, manifest_file) + verbose_print("\n--- Sync Complete ---") + verbose_print(f"New: {stats['new']}, Updated: {stats['updated']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}, Pruned: {pruned_count}") + verbose_print("--------------------") + if stats["failed"] > 0: + log_warning(f"{stats['failed']} image(s) failed to sync. Check the build log for details.") + +if __name__ == "__main__": + main() \ No newline at end of file