├── Meshtastic ├── ShowTime.swift ├── Resources │ ├── alpha.png │ ├── Assets.xcassets │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── tbeam.imageset │ │ │ │ ├── tbeam.jpg │ │ │ │ ├── tbeam-1.jpg │ │ │ │ ├── tbeam-2.jpg │ │ │ │ └── Contents.json │ │ │ ├── techo.imageset │ │ │ │ ├── techo.jpg │ │ │ │ ├── techo-1.jpg │ │ │ │ ├── techo-2.jpg │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 1024.png │ │ │ │ ├── 120.png │ │ │ │ ├── 152.png │ │ │ │ ├── 167.png │ │ │ │ ├── 180.png │ │ │ │ ├── 20.png │ │ │ │ ├── 29.png │ │ │ │ ├── 40-1.png │ │ │ │ ├── 40-2.png │ │ │ │ ├── 40.png │ │ │ │ ├── 58-1.png │ │ │ │ ├── 58.png │ │ │ │ ├── 60.png │ │ │ │ ├── 76.png │ │ │ │ ├── 80-1.png │ │ │ │ ├── 80.png │ │ │ │ ├── 87.png │ │ │ │ └── 120-1.png │ │ │ ├── tlorav1.imageset │ │ │ │ ├── tlora.jpeg │ │ │ │ ├── tlora-1.jpeg │ │ │ │ ├── tlora-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── HELTECV20.imageset │ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6.png │ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png │ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png │ │ │ │ └── Contents.json │ │ │ ├── rak4631.imageset │ │ │ │ ├── RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive.png │ │ │ │ ├── RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive-1.png │ │ │ │ └── Contents.json │ │ │ ├── UNSET.imageset │ │ │ │ └── Contents.json │ │ │ ├── TLORAV2.imageset │ │ │ │ └── Contents.json │ │ │ ├── TLORAV211p6.imageset │ │ │ │ └── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── Color.colorset │ │ │ │ └── Contents.json │ │ └── TBEAM.imageset │ │ │ ├── tbeam.jpg │ │ │ └── tbeam-1.jpg │ ├── AppIcon_Ham.icon │ │ ├── Assets │ │ │ └── ham.png │ │ └── icon.json │ ├── AppIcon.icon │ │ └── Assets │ │ │ ├── Icon Grid Inner Circle.svg │ │ │ ├── Icon Grid Middle Stroke 3.svg │ │ │ ├── Icon Grid Outer Stroke 2.svg │ │ │ ├── Icon Grid Middle Circle.svg │ │ │ ├── Icon Grid Outer Circle.svg │ │ │ ├── Meshtastic Logo.svg │ │ │ └── Icon Grid Inner Stroke.svg │ ├── AppIconDebug.icon │ │ └── Assets │ │ │ ├── Icon Grid Inner Circle.svg │ │ │ ├── Icon Grid Middle Stroke 3.svg │ │ │ ├── Icon Grid Outer Stroke 2.svg │ │ │ ├── Icon Grid Middle Circle.svg │ │ │ ├── Icon Grid Outer Circle.svg │ │ │ ├── Meshtastic Logo.svg │ │ │ └── Icon Grid Inner Stroke.svg │ └── AppIcon_Chirpy.icon │ │ └── icon.json ├── Assets.xcassets │ ├── Contents.json │ ├── UNPHONE.imageset │ │ ├── UNPHONE.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── logo-3.png │ │ ├── logo-dark.png │ │ ├── logo-tinted.png │ │ └── Contents.json │ ├── AppIconDebug.appiconset │ │ ├── DevIcon.png │ │ ├── DevIcon-Dark.png │ │ ├── DevIcon-Tinted.png │ │ └── Contents.json │ ├── SOLAR_NODE.imageset │ │ ├── solar_node.png │ │ └── Contents.json │ ├── AppIcon_Ham.appiconset │ │ ├── logo-dark 1.png │ │ ├── logo-dark.png │ │ ├── meshtastic_ham.jpg │ │ └── Contents.json │ ├── AppIcon_MPowered.appiconset │ │ ├── MPowered.png │ │ ├── MPowered 1.png │ │ ├── MPowered 2.png │ │ └── Contents.json │ ├── AppIcon_Thumb.imageset │ │ ├── AppIcon_Thumb.png │ │ └── Contents.json │ ├── AppIcon_Chirpy.appiconset │ │ ├── AppIcon_Chirpy.png │ │ ├── AppIcon_Chirpy_Dark.png │ │ └── Contents.json │ ├── AppIcon_Ham_Dark_Thumb.imageset │ │ ├── logo-dark.png │ │ └── Contents.json │ ├── AppIcon_Ham_Thumb.imageset │ │ ├── meshtastic_ham.jpg │ │ └── Contents.json │ ├── AppIcon_MPowered_Thumb.imageset │ │ ├── MPowered.png │ │ └── Contents.json │ ├── ANDROIDSIM.imageset │ │ ├── play_store_icon_114px-4.png │ │ └── Contents.json │ ├── AppIcon_Dark_Thumb.imageset │ │ ├── AppIcon_Dark_Thumb.png │ │ └── Contents.json │ ├── AppIcon_MPowered_Dark_Thumb.imageset │ │ ├── MPowered.png │ │ └── Contents.json │ ├── AppIcon_Chirpy_Thumb.imageset │ │ ├── AppIcon_Chirpy_Thumb.png │ │ └── Contents.json │ ├── Symbol.symbolset │ │ └── Contents.json │ ├── AppIcon_Chirpy_Dark_Thumb.imageset │ │ ├── AppIcon_Chirpy_Dark_Thumb.png │ │ └── Contents.json │ ├── STATIONG1.imageset │ │ ├── meshtastic_mesh_device_station_edition_overview 1.png │ │ └── Contents.json │ ├── RPIPICO.imageset │ │ └── Contents.json │ ├── TBEAM.imageset │ │ └── Contents.json │ ├── TDECK.imageset │ │ └── Contents.json │ ├── TECHO.imageset │ │ └── Contents.json │ ├── UNSET.imageset │ │ └── Contents.json │ ├── HELTECV4.imageset │ │ └── Contents.json │ ├── PROMICRO.imageset │ │ └── Contents.json │ ├── RAK11310.imageset │ │ └── Contents.json │ ├── TLORAC6.imageset │ │ └── Contents.json │ ├── HELTECV3.imageset │ │ └── Contents.json │ ├── MUZIR1NEO.imageset │ │ └── Contents.json │ ├── RAK4631.imageset │ │ └── Contents.json │ ├── STATIONG2.imageset │ │ └── Contents.json │ ├── TWATCHS3.imageset │ │ └── Contents.json │ ├── HELTECWSLV3.imageset │ │ └── Contents.json │ ├── NANOG2ULTRA.imageset │ │ └── Contents.json │ ├── SEEEDSOLARNODE.imageset │ │ └── Contents.json │ ├── SEEEDXIAOS3.imageset │ │ └── Contents.json │ ├── THINKNODEM1.imageset │ │ └── Contents.json │ ├── THINKNODEM2.imageset │ │ └── Contents.json │ ├── THINKNODEM3.imageset │ │ └── Contents.json │ ├── THINKNODEM4.imageset │ │ └── Contents.json │ ├── TLORAT3S3V1.imageset │ │ └── Contents.json │ ├── TLORAV211P6.imageset │ │ └── Contents.json │ ├── TLORAV211P8.imageset │ │ └── Contents.json │ ├── WIOWM1110.imageset │ │ └── Contents.json │ ├── WISMESHTAP.imageset │ │ └── Contents.json │ ├── logo-black.imageset │ │ ├── Contents.json │ │ └── Mesh_Logo_Black.svg │ ├── logo-white.imageset │ │ ├── Contents.json │ │ └── Mesh_Logo_White.svg │ ├── LILYGOTBEAMS3CORE.imageset │ │ └── Contents.json │ ├── NANOG1.imageset │ │ ├── 2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png │ │ └── Contents.json │ ├── TLORAT3S3EPAPER.imageset │ │ └── Contents.json │ ├── TRACKERT1000E.imageset │ │ └── Contents.json │ ├── XIAONRF52KIT.imageset │ │ └── Contents.json │ ├── HELTECHT62.imageset │ │ └── Contents.json │ ├── HELTECMESHPOCKET.imageset │ │ └── Contents.json │ ├── SEEEDWIOTRACKERL1.imageset │ │ └── Contents.json │ ├── custom.bluetooth.symbolset │ │ └── Contents.json │ ├── custom.link.slash.symbolset │ │ └── Contents.json │ ├── soil.moisture.symbolset │ │ └── Contents.json │ ├── soil.temperature.symbolset │ │ └── Contents.json │ ├── HELTECWIRELESSPAPER.imageset │ │ └── Contents.json │ ├── SENSECAPINDICATOR.imageset │ │ └── Contents.json │ ├── progress.ring.dashed.symbolset │ │ └── Contents.json │ ├── HELTECMESHNODET114.imageset │ │ └── Contents.json │ ├── HELTECVISIONMASTERE213.imageset │ │ └── Contents.json │ ├── HELTECVISIONMASTERE290.imageset │ │ └── Contents.json │ ├── HELTECWIRELESSTRACKER.imageset │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── Color.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RELEASENOTES.md ├── Meshtastic.xcdatamodeld │ └── .xccurrentversion ├── Extensions │ ├── Protobufs │ │ └── NodeInfoExtensions.swift │ ├── Character.swift │ ├── Data.swift │ ├── Bool.swift │ ├── Double.swift │ ├── Int.swift │ ├── CoreData │ │ ├── RangeTestConfigEntityExtension.swift │ │ ├── StoreForwardConfigEntityExtension.swift │ │ ├── LocationEntityExtension.swift │ │ ├── TraceRouteEntityExtension.swift │ │ ├── SerialConfigEntityExtension.swift │ │ ├── DeviceMetadataEntityExtension.swift │ │ ├── MQTTConfigEntityExtension.swift │ │ └── MyInfoEntityExtension.swift │ ├── Measurement.swift │ ├── Constants.swift │ ├── OSLogEntryLog.swift │ ├── UIImage.swift │ ├── CLLocation.swift │ ├── Date.swift │ ├── Bundle.swift │ ├── Float.swift │ └── UIColor.swift ├── Enums │ ├── MessageDestination.swift │ ├── EthernetModes.swift │ ├── TelemetryWeather.swift │ ├── ChannelRoles.swift │ ├── BluetoothModes.swift │ ├── RouteEnums.swift │ ├── KeyBackupStatus.swift │ ├── MessagingEnums.swift │ └── DetectionSensorEnums.swift ├── Views │ ├── Messages │ │ ├── TextMessageField │ │ │ ├── AlertButton.swift │ │ │ ├── RequestPositionButton.swift │ │ │ └── TextMessageSize.swift │ │ └── TapbackResponses.swift │ ├── Nodes │ │ └── Helpers │ │ │ ├── ScrollToBottomButton.swift │ │ │ ├── Actions │ │ │ ├── NodeAlertsButton.swift │ │ │ ├── ClientHistoryButton.swift │ │ │ ├── TraceRouteButton.swift │ │ │ └── IgnoreNodeButton.swift │ │ │ ├── PreferenceKeys │ │ │ └── TileHeightKeys.swift │ │ │ └── NodeFilterParameters.swift │ ├── Helpers │ │ ├── Weather │ │ │ └── IAQScale.swift │ │ ├── DistanceText.swift │ │ ├── DateTimeText.swift │ │ ├── Messages │ │ │ └── MessageTemplate.swift │ │ ├── Help │ │ │ └── AckErrors.swift │ │ ├── Compact Widgets │ │ │ ├── WeightCompactWidget.swift │ │ │ ├── DistanceCompactWidget.swift │ │ │ ├── RadiationCompactWidget.swift │ │ │ ├── CurrentConditionsCompact.swift │ │ │ ├── PressureCompactWidget.swift │ │ │ ├── WeatherConditionsCompactWidget.swift │ │ │ ├── HumidityCompactWidget.swift │ │ │ └── WindCompactWidget.swift │ │ ├── ChannelLock.swift │ │ └── MeshtasticLogo.swift │ └── Settings │ │ ├── AppIconPicker.swift │ │ ├── AppIconButton.swift │ │ └── Config │ │ └── ConfigHeader.swift ├── Helpers │ ├── CommonRegex.swift │ ├── BluetoothManager.swift │ ├── Preferences.swift │ ├── Logger.swift │ └── EmojiOnlyTextField.swift ├── Tips │ ├── BluetoothTips.swift │ ├── MessagesTips.swift │ ├── PersistantTips.swift │ └── ChannelTips.swift ├── Measurement │ └── CustomFormatters.swift ├── Export │ ├── LogDocument.swift │ └── CsvDocument.swift ├── Model │ ├── PeripheralModel.swift │ └── CoreData │ │ └── TelemetryEntity+CoreDataProperties.swift ├── AppIntents │ ├── AppIntentErrors.swift │ ├── DisconnectNodeIntent.swift │ ├── TracerouteIntent.swift │ ├── RestartNodeIntent.swift │ ├── ShutDownNodeIntent.swift │ ├── MessageNodeIntent.swift │ ├── ShortcutsProvider.swift │ └── AddContactIntent.swift ├── Accessory │ ├── Helpers │ │ ├── LogRecord+StringRepresentation.swift │ │ └── ManualConnectionList.swift │ └── Protocols │ │ └── Connection.swift ├── AppState.swift ├── Meshtastic.entitlements └── Router │ └── NavigationState.swift ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature.yml ├── workflows │ ├── swiftlint.yml │ └── stale.yml ├── FUNDING.yml └── pull_request_template.md ├── scripts ├── hooks │ └── pre-commit ├── thebenternify.sh ├── unthebenternify.sh ├── gen_protos.sh ├── setup-hooks.sh └── create-release-branch.sh ├── meshtastic-1080x1080.png ├── .gitmodules ├── Widgets ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── WidgetBackground.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── LightIndigo.colorset │ │ └── Contents.json │ ├── AccentColorDimmed.colorset │ │ └── Contents.json │ ├── m-logo-black.imageset │ │ ├── Contents.json │ │ ├── Mesh_Logo_Black.svg │ │ ├── Mesh_Logo_Black_Large.svg │ │ └── Mesh_Logo_Black_Small.svg │ ├── m-logo-white.imageset │ │ ├── Contents.json │ │ ├── Mesh_Logo_White.svg │ │ ├── Mesh_Logo_White_Large.svg │ │ └── Mesh_Logo_White_Small.svg │ └── LiveActivityBackground.colorset │ │ └── Contents.json ├── WidgetsExtension.entitlements ├── Info.plist ├── WidgetsBundle.swift └── MeshActivityAttributes.swift ├── Settings.bundle ├── en.lproj │ └── Root.strings └── de.lproj │ └── Root.strings ├── MeshtasticProtobufs ├── .gitignore ├── Package.resolved └── Package.swift ├── Meshtastic.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── WorkspaceSettings.xcsettings │ └── IDEWorkspaceChecks.plist ├── Meshtastic.xcworkspace ├── xcshareddata │ ├── WorkspaceSettings.xcsettings │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── ci_scripts └── ci_pre_xcodebuild.sh ├── README.md ├── .swiftlint-precommit.yml ├── .swiftlint.yml └── RELEASING.md /Meshtastic/ShowTime.swift: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /scripts/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./scripts/lint/lint-fix-changes.sh -------------------------------------------------------------------------------- /meshtastic-1080x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/meshtastic-1080x1080.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "protobufs"] 2 | path = protobufs 3 | url = https://github.com/meshtastic/protobufs.git 4 | -------------------------------------------------------------------------------- /Meshtastic/Resources/alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/alpha.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Settings.bundle/en.lproj/Root.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Settings.bundle/en.lproj/Root.strings -------------------------------------------------------------------------------- /Meshtastic/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon_Ham.icon/Assets/ham.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/AppIcon_Ham.icon/Assets/ham.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-3.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam.jpg -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark 1.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam-1.jpg -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon-Dark.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Thumb.imageset/AppIcon_Thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Thumb.imageset/AppIcon_Thumb.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon-Tinted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIconDebug.appiconset/DevIcon-Tinted.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/meshtastic_ham.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/meshtastic_ham.jpg -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 1.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 2.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy.appiconset/AppIcon_Chirpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Chirpy.appiconset/AppIcon_Chirpy.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/logo-dark.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/meshtastic_ham.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/meshtastic_ham.jpg -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/MPowered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/MPowered.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy.appiconset/AppIcon_Chirpy_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Chirpy.appiconset/AppIcon_Chirpy_Dark.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Dark_Thumb.imageset/AppIcon_Dark_Thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Dark_Thumb.imageset/AppIcon_Dark_Thumb.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/MPowered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/MPowered.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40-1.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40-2.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/58-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/58-1.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/80-1.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam-1.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam-2.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo-1.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo-2.jpg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora.jpeg -------------------------------------------------------------------------------- /MeshtasticProtobufs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Meshtastic.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy_Thumb.imageset/AppIcon_Chirpy_Thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Chirpy_Thumb.imageset/AppIcon_Chirpy_Thumb.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-1.jpeg -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-2.jpeg -------------------------------------------------------------------------------- /scripts/thebenternify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -i '' -e 's/GCH7VS5Y9R/6YF6QJH524/g' ./Meshtastic.xcodeproj/project.pbxproj 4 | sed -i '' -e 's/gvh.Meshtastic/thebentern.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj 5 | -------------------------------------------------------------------------------- /scripts/unthebenternify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -i '' -e 's/6YF6QJH524/GCH7VS5Y9R/g' ./Meshtastic.xcodeproj/project.pbxproj 4 | sed -i '' -e 's/thebentern.Meshtastic/gvh.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj 5 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/Symbol.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "idiom" : "universal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy_Dark_Thumb.imageset/AppIcon_Chirpy_Dark_Thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/AppIcon_Chirpy_Dark_Thumb.imageset/AppIcon_Chirpy_Dark_Thumb.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png -------------------------------------------------------------------------------- /Meshtastic.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pico.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tbeam.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "t-deck.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "t-echo.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/UNSET.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "unknown.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECV4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec_v4.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "promicro.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rak11310.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-c6.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/UNPHONE.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "UNPHONE.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-v3-case.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "muzi_r1_neo.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rak4631_case.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "solar_node.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "station-g2.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "t-watch-s3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-wsl-v3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "nano-g2-ultra.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seeed_solar.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seeed-xiao-s3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thinknode_m1.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thinknode_m2.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/THINKNODEM3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thinknode_m3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/THINKNODEM4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thinknode_m4.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-t3s3-v1.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TLORAV211P6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-v2-1-1_6.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TLORAV211P8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-v2-1-1_8.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wio-tracker-wm1110.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rak-wismeshtap.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mesh_Logo_Black.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mesh_Logo_White.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tbeam-s3-core.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-t3s3-epaper.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tracker-t1000-e.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seeed_xiao_nrf52_kit.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "play_store_icon_114px-4.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-ht62-esp32c3-sx1262.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec_mesh_pocket.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wio_tracker_l1_case.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/custom.bluetooth.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "custom.bluetooth.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/custom.link.slash.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "custom.link.slash.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/soil.moisture.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "soilMoisture.variable.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/soil.temperature.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "soilTemp.variable.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-wireless-paper.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seeed-sensecap-indicator.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "progress.ring.dashed.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-mesh-node-t114-case.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-vision-master-e213.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-vision-master-e290.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heltec-wireless-tracker.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/STATIONG1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "meshtastic_mesh_device_station_edition_overview 1.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/NANOG1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | # 1.27.8 2 | 3 | * Update NodeList SwipeAction Button to be role: Destructive 4 | * Added com.apple.security.files.user-selected.read-write entitlement to AppSandbox for MacOS for Mesh log download 5 | * Cleaned up bluetooth connecting timeout errors and logic, run 10 2 second timers now 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | MeshtasticDataModelV 55.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MeshtasticProtobufs 3 | 4 | extension NodeInfo { 5 | var isValidPosition: Bool { 6 | hasPosition && 7 | position.longitudeI != 0 && 8 | position.latitudeI != 0 && 9 | position.latitudeI != 373346000 && 10 | position.longitudeI != -1220090000 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Widgets/WidgetsExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Widgets/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Meshtastic.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/rak4631.imageset/RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/rak4631.imageset/RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive.png -------------------------------------------------------------------------------- /Widgets/WidgetsBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetsBundle.swift 3 | // Widgets 4 | // 5 | // Created by Garth Vander Houwen on 2/28/23. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct WidgetsBundle: WidgetBundle { 13 | var body: some Widget { 14 | // Widgets() 15 | #if canImport(ActivityKit) 16 | WidgetsLiveActivity() 17 | #endif 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/rak4631.imageset/RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/HEAD/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/rak4631.imageset/RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive-1.png -------------------------------------------------------------------------------- /Meshtastic/Extensions/Character.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Character.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Character { 11 | var isEmoji: Bool { 12 | guard let scalar = unicodeScalars.first else { return false } 13 | return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/UNSET.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/TLORAV2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/TLORAV211p6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | var macAddressString: String { 12 | let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} 13 | return String(mac.dropLast()) 14 | } 15 | var hexDescription: String { 16 | return reduce("") {$0 + String(format: "%02x", $1)} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_Thumb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "meshtastic_ham.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MPowered.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/LightIndigo.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.56", 9 | "green" : "0.96", 10 | "red" : "0.32" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Dark_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_Dark_Thumb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MPowered.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_Chirpy_Thumb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/AccentColorDimmed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.420", 9 | "green" : "0.470", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy_Dark_Thumb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_Chirpy_Dark_Thumb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MeshtasticProtobufs/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "a2385deee281bd55bce80722a1f2b020f7b745c02005befa8ccbf58a39ef4002", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-protobuf", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-protobuf.git", 8 | "state" : { 9 | "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", 10 | "version" : "1.29.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | SwiftLint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: GitHub Action for SwiftLint (Only files changed in the PR) 16 | uses: norio-nomura/action-swiftlint@3.2.1 17 | env: 18 | DIFF_BASE: ${{ github.base_ref }} -------------------------------------------------------------------------------- /Meshtastic/Enums/MessageDestination.swift: -------------------------------------------------------------------------------- 1 | /// Helper abstraction for sharing functionality between channel and direct messaging. 2 | enum MessageDestination { 3 | case user(UserEntity) 4 | case channel(ChannelEntity) 5 | 6 | var userNum: Int64 { 7 | switch self { 8 | case let .user(user): return user.num 9 | case .channel: return 0 10 | } 11 | } 12 | 13 | var channelNum: Int32 { 14 | switch self { 15 | case .user: return 0 16 | case let .channel(channel): return channel.index 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Meshtastic/Views/Messages/TextMessageField/AlertButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AlertButton: View { 4 | let action: () -> Void 5 | 6 | var body: some View { 7 | Button(action: action) { 8 | Text("Alert") 9 | Image(systemName: "bell.fill") 10 | .symbolRenderingMode(.hierarchical) 11 | .imageScale(.large) 12 | .foregroundColor(.accentColor) 13 | } 14 | } 15 | } 16 | 17 | struct AlertButtonPreview: PreviewProvider { 18 | static var previews: some View { 19 | AlertButton {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic/Helpers/CommonRegex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonRegex.swift 3 | // Meshtastic 4 | // 5 | // Created by Ben Meadors on 7/2/24. 6 | // 7 | 8 | import Foundation 9 | import RegexBuilder 10 | 11 | class CommonRegex { 12 | static let COORDS_REGEX = Regex { 13 | Capture { 14 | Regex { 15 | "lat=" 16 | OneOrMore(.digit) 17 | } 18 | } 19 | Capture {" "} 20 | Capture { 21 | Regex { 22 | "long=" 23 | OneOrMore(.digit) 24 | } 25 | } 26 | } 27 | .anchorsMatchLineEndings() 28 | } 29 | -------------------------------------------------------------------------------- /scripts/gen_protos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # simple sanity checking for executable 4 | if [ ! -x "$(which protoc)" ]; then 5 | brew install swift-protobuf 6 | fi 7 | 8 | git submodule update --init --recursive 9 | 10 | git submodule foreach --recursive git pull origin master 11 | 12 | protoc --proto_path=./protobufs --swift_opt=Visibility=Public --swift_out=./MeshtasticProtobufs/Sources ./protobufs/meshtastic/*.proto 13 | 14 | echo "Done generating the swift files from the proto files." 15 | echo "Build, test, and commit changes." 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tbeam-2.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "tbeam-1.jpg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "tbeam.jpg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "techo-2.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "techo-1.jpg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "techo.jpg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tlora-2.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "tlora-1.jpeg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "tlora.jpeg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mesh_Logo_Black_Small.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Mesh_Logo_Black.svg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Mesh_Logo_Black_Large.svg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mesh_Logo_White_Small.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Mesh_Logo_White.svg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Mesh_Logo_White_Large.svg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/6/25. 6 | 7 | extension Bool { 8 | 9 | static var iOS18: Bool { 10 | guard #available(iOS 18, *) else { 11 | return true 12 | } 13 | return false 14 | } 15 | 16 | static var masOS15: Bool { 17 | guard #available(macOS 15, *) else { 18 | return true 19 | } 20 | return false 21 | } 22 | 23 | static var os26: Bool { 24 | guard #available(iOS 26, macOS 26, *) else { 25 | return true 26 | } 27 | return false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Double.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen on 4/25/23. 6 | // 7 | import Foundation 8 | 9 | extension Double { 10 | var toBytes: String { 11 | let formatter = MeasurementFormatter() 12 | let measurement = Measurement(value: self, unit: UnitInformationStorage.bytes) 13 | formatter.unitStyle = .short 14 | formatter.unitOptions = .naturalScale 15 | formatter.numberFormatter.maximumFractionDigits = 0 16 | return formatter.string(from: measurement.converted(to: .megabytes)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Int.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | extension Int { 9 | 10 | func numberOfDigits() -> Int { 11 | if abs(self) < 10 { 12 | return 1 13 | } else { 14 | return 1 + (self/10).numberOfDigits() 15 | } 16 | } 17 | } 18 | 19 | extension UInt32 { 20 | func toHex() -> String { 21 | return String(format: "!%08X", self).lowercased() 22 | } 23 | } 24 | 25 | extension Int64 { 26 | func toHex() -> String { 27 | return String(format: "!%08X", self).lowercased() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import MeshtasticProtobufs 3 | 4 | extension RangeTestConfigEntity { 5 | convenience init( 6 | context: NSManagedObjectContext, 7 | config: ModuleConfig.RangeTestConfig 8 | ) { 9 | self.init(context: context) 10 | self.sender = Int32(config.sender) 11 | self.enabled = config.enabled 12 | self.save = config.save 13 | } 14 | 15 | func update(with config: ModuleConfig.RangeTestConfig) { 16 | sender = Int32(config.sender) 17 | enabled = config.enabled 18 | save = config.save 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic/Tips/BluetoothTips.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothTips.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/31/23. 6 | // 7 | import SwiftUI 8 | import TipKit 9 | 10 | struct ConnectionTip: Tip { 11 | 12 | var id: String { 13 | return "tip.connect" 14 | } 15 | var title: Text { 16 | Text("Connected Radio") 17 | } 18 | var message: Text? { 19 | Text("Shows information for the connected Lora radio. You can swipe left to disconnect the radio and long press to start the live activity.") 20 | } 21 | var image: Image? { 22 | Image(systemName: "flipphone") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic/Measurement/CustomFormatters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomFormatters.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 8/4/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Custom altitude formatter that always returns the provided unit 11 | /// Needs to be used in conjunction with logic that checks for metric and displays the right value. 12 | public var altitudeFormatter: MeasurementFormatter { 13 | let formatter = MeasurementFormatter() 14 | formatter.unitOptions = .providedUnit 15 | formatter.unitStyle = .long 16 | formatter.numberFormatter.maximumFractionDigits = 1 17 | return formatter 18 | } 19 | -------------------------------------------------------------------------------- /scripts/setup-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Define the source and destination paths 5 | SOURCE_PATH="./scripts/hooks/pre-commit" 6 | HOOKS_DIR=".git/hooks" 7 | DEST_PATH="$HOOKS_DIR/pre-commit" 8 | 9 | # Check if the hooks directory exists 10 | if [ ! -d "$HOOKS_DIR" ]; then 11 | echo "Error: .git/hooks directory not found. Make sure you're in the root of a Git repository." 12 | exit 1 13 | fi 14 | 15 | # Copy the script to the hooks directory 16 | cp "$SOURCE_PATH" "$DEST_PATH" 17 | 18 | # Make the hook script executable 19 | chmod +x "$DEST_PATH" 20 | 21 | echo "Pre-commit hooks have been set up successfully." 22 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "655DCEC0-309D-430A-AF50-2453B6ADB1F6.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RequestPositionButton: View { 4 | let action: () -> Void 5 | 6 | var body: some View { 7 | Button(action: action) { 8 | Image(systemName: "mappin.and.ellipse") 9 | .accessibilityLabel("Position Exchange Requested".localized) 10 | .symbolRenderingMode(.hierarchical) 11 | .imageScale(.large) 12 | .foregroundColor(.accentColor) 13 | } 14 | .padding(.trailing) 15 | } 16 | } 17 | 18 | struct RequestPositionButtonPreview: PreviewProvider { 19 | static var previews: some View { 20 | RequestPositionButton {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Measurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Measurement.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 11/17/23. 6 | // 7 | 8 | import Foundation 9 | import Charts 10 | 11 | struct PlottableMeasurement { 12 | var measurement: Measurement 13 | } 14 | 15 | extension PlottableMeasurement: Plottable where UnitType == UnitLength { 16 | var primitivePlottable: Double { 17 | self.measurement.converted(to: .meters).value 18 | } 19 | 20 | init?(primitivePlottable: Double) { 21 | self.init( 22 | measurement: Measurement( 23 | value: primitivePlottable, 24 | unit: .meters 25 | ) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: process stale Issues and PR's 2 | on: 3 | schedule: 4 | - cron: 0 6 * * * 5 | workflow_dispatch: {} 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | actions: write 11 | 12 | jobs: 13 | stale_issues: 14 | name: Close Stale Issues 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Stale PR+Issues 19 | uses: actions/stale@v9.0.0 20 | with: 21 | days-before-stale: 30 22 | exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue' 23 | exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue' 24 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/rak4631.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive-1.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic/Tips/MessagesTips.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagesTips.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/15/23. 6 | // 7 | import SwiftUI 8 | import TipKit 9 | 10 | struct MessagesTip: Tip { 11 | 12 | var id: String { 13 | return "tip.messages" 14 | } 15 | var title: Text { 16 | Text("Messages") 17 | } 18 | var message: Text? { 19 | Text("You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details.") 20 | } 21 | var image: Image? { 22 | Image(systemName: "bubble.left.and.bubble.right") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MeshtasticProtobufs/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "MeshtasticProtobufs", 7 | products: [ 8 | .library( 9 | name: "MeshtasticProtobufs", 10 | targets: ["MeshtasticProtobufs"] 11 | ), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.33.3"), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "MeshtasticProtobufs", 19 | dependencies: [ 20 | .product(name: "SwiftProtobuf", package: "swift-protobuf") 21 | ] 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ci_scripts/ci_pre_xcodebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Stage: PRE-Xcode Build is activated .... " 4 | 5 | # Move to the place where the scripts are located. 6 | # This is important because the position of the subsequently mentioned files depend of this origin. 7 | cd $CI_PRIMARY_REPOSITORY_PATH/ci_scripts || exit 1 8 | 9 | # Write a JSON File containing all the environment variables and secrets. 10 | printf "{\"PUBLIC_MQTT_USERNAME\":\"%s\",\"PUBLIC_MQTT_PASSWORD\":\"%s\"}" "$PUBLIC_MQTT_USERNAME" "$PUBLIC_MQTT_PASSWORD" >> .\ $CI_PRIMARY_REPOSITORY_PATH/SupportingFiles/secrets.json 11 | 12 | echo "Wrote Secrets.json file." 13 | 14 | echo "Stage: PRE-Xcode Build is DONE .... " 15 | 16 | exit 0 17 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/ScrollToBottomButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollToBottomButtonView.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 4/2/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScrollToBottomButtonView: View { 11 | var body: some View { 12 | HStack(spacing: 4) { 13 | Text("Jump to present") 14 | .font(.caption) 15 | .padding(.horizontal, 8) 16 | .padding(.vertical, 4) 17 | .cornerRadius(12) 18 | Image(systemName: "arrow.down") 19 | .font(.title2) 20 | .symbolRenderingMode(.hierarchical) 21 | 22 | } 23 | .foregroundColor(.accentColor) 24 | .shadow(radius: 2) 25 | } 26 | } 27 | 28 | #Preview { 29 | ScrollToBottomButtonView() 30 | } 31 | -------------------------------------------------------------------------------- /Settings.bundle/de.lproj/Root.strings: -------------------------------------------------------------------------------- 1 | // german localization 2 | 3 | "Will share your phone GPS location with your node." = "Teile den GPS-Standort deines Handys mit deinem Funkgerät."; 4 | "Share Location" = "Standort teilen"; 5 | "Interval" = "Intervall"; 6 | "Ten Seconds" = "10 s"; 7 | "Fifteen Seconds" = "15 s"; 8 | "Thirty Seconds" = "30 s"; 9 | "Forty Five Seconds" = "45 s"; 10 | "One Minute" = "1 min"; 11 | "Five Minutes" = "5 min"; 12 | "Ten Minutes" = "10 min"; 13 | "Fifteen Minutes" = "15 min"; 14 | "Accurate Locations Only" = "Nur genaue Standorte"; 15 | "Notifications" = "Mitteilungen"; 16 | "Channel Messages" = "Kanalnachrichten"; 17 | "New Nodes" = "Neu entdeckte Knoten"; 18 | "Low Battery" = "Niedriger Akkustand"; 19 | -------------------------------------------------------------------------------- /Meshtastic/Helpers/BluetoothManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothManager.swift 3 | // MeshtasticClient 4 | // 5 | // Created by Garth Vander Houwen on 12/1/21. 6 | // 7 | 8 | import Combine 9 | import CoreBluetooth 10 | 11 | final class BluetoothManager: NSObject { 12 | 13 | private var centralManager: CBCentralManager! 14 | 15 | var stateSubject: PassthroughSubject = .init() 16 | var peripheralSubject: PassthroughSubject = .init() 17 | 18 | func start() { 19 | centralManager = .init(delegate: self, queue: .main) 20 | } 21 | 22 | func connect(_ peripheral: CBPeripheral) { 23 | centralManager.stopScan() 24 | peripheral.delegate = self 25 | centralManager.connect(peripheral) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Meshtastic/Export/LogDocument.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | 4 | struct LogDocument: FileDocument { 5 | static var readableContentTypes: [UTType] {[.plainText]} 6 | 7 | var logFile: String 8 | 9 | init(logFile: String) { 10 | self.logFile = logFile 11 | } 12 | 13 | init(configuration: ReadConfiguration) throws { 14 | guard let data = configuration.file.regularFileContents, 15 | let string = String(data: data, encoding: .utf8) 16 | else { 17 | throw CocoaError(.fileReadCorruptFile) 18 | } 19 | logFile = string 20 | } 21 | 22 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { 23 | return FileWrapper(regularFileWithContents: logFile.data(using: .utf8)!) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Constants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | enum Constants { 4 | /// `UInt32.max` or FFFF,FFFF in hex is used to identify messages that are being 5 | /// sent to a channel and are not a DM to an individual user. This is used 6 | /// in the `to` field of some mesh packets. 7 | static let maximumNodeNum = UInt32.max 8 | /// Based on the NUM_RESERVED from the firmware. 9 | /// https://github.com/meshtastic/firmware/blob/46d7b82ac1a4292ba52ca690e1a433d3a501a9e5/src/mesh/NodeDB.cpp#L522 10 | static let minimumNodeNum = 4 11 | 12 | // String used to render a nil value. If changed, mark the new entry in 13 | // Localizable.xcstrings as "do not translate" and remove the old key. 14 | static let nilValueIndicator = "--" 15 | } 16 | -------------------------------------------------------------------------------- /Meshtastic/Model/PeripheralModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreBluetooth 3 | 4 | struct Peripheral: Identifiable { 5 | var id: String 6 | var num: Int64 7 | var name: String 8 | var shortName: String 9 | var longName: String 10 | var firmwareVersion: String 11 | var rssi: Int 12 | var lastUpdate: Date 13 | var peripheral: CBPeripheral 14 | 15 | func getSignalStrength() -> BLESignalStrength { 16 | if NSNumber(value: rssi).compare(NSNumber(-65)) == ComparisonResult.orderedDescending { 17 | return BLESignalStrength.strong 18 | } else if NSNumber(value: rssi).compare(NSNumber(-85)) == ComparisonResult.orderedDescending { 19 | return BLESignalStrength.normal 20 | } else { 21 | return BLESignalStrength.weak 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic/Helpers/Preferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preferences.swift 3 | // MeshtasticClient 4 | // 5 | // Created by Garth Vander Houwen on 12/16/21. 6 | // 7 | // import Foundation 8 | // import Combine 9 | // import SwiftUI 10 | // 11 | // class Prefs 12 | // { 13 | // private let defaults = UserDefaults.standard 14 | // 15 | // private let keyIntExample = "intExample" 16 | // 17 | // var intExample = { 18 | // set { 19 | // defaults.setValue(newValue, forKey: keyIntExample) 20 | // } 21 | // get { 22 | // return defaults.integer(forKey: keyIntExample) 23 | // } 24 | // } 25 | // 26 | // class var shared: Prefs { 27 | // struct Static { 28 | // static let instance = Prefs() 29 | // } 30 | // 31 | // return Static.instance 32 | // } 33 | // } 34 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/AppIntentErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIntentErrors.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 8/11/24. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | class AppIntentErrors { 12 | enum AppIntentError: Swift.Error, CustomLocalizedStringResourceConvertible { 13 | case notConnected 14 | case message(_ message: String) 15 | 16 | var localizedStringResource: LocalizedStringResource { 17 | switch self { 18 | case let .message(message): 19 | Logger.services.error("App Intent: \(message, privacy: .public)") 20 | return "Error: \(message)" 21 | case .notConnected: 22 | Logger.services.error("App Intent: No Connected Node") 23 | return "No Connected Node" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Meshtastic/Helpers/Logger.swift: -------------------------------------------------------------------------------- 1 | import OSLog 2 | 3 | extension Logger { 4 | 5 | /// The logger's subsystem. 6 | private static var subsystem = Bundle.main.bundleIdentifier! 7 | 8 | /// All logs related to data such as decoding error, parsing issues, etc. 9 | public static let data = Logger(subsystem: subsystem, category: "🗄️ Data") 10 | 11 | /// All logs related to the mesh 12 | public static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh") 13 | 14 | /// All logs related to services such as network calls, location, etc. 15 | public static let services = Logger(subsystem: subsystem, category: "🍏 Services") 16 | 17 | /// All logs related to tracking and analytics. 18 | public static let statistics = Logger(subsystem: subsystem, category: "📈 Stats") 19 | } 20 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Weather/IAQScale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAQScale.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 4/24/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct IAQScale: View { 11 | 12 | var body: some View { 13 | VStack(alignment: .leading) { 14 | Text("Indoor Air Quality (IAQ)") 15 | .font(.title3) 16 | ForEach(Iaq.allCases) { iaq in 17 | HStack { 18 | RoundedRectangle(cornerRadius: 5) 19 | .fill(iaq.color) 20 | .frame(width: 30, height: 20) 21 | Text(iaq.description) 22 | .font(.callout) 23 | } 24 | } 25 | } 26 | .padding() 27 | } 28 | } 29 | 30 | struct IAQSCalePreviews: PreviewProvider { 31 | static var previews: some View { 32 | VStack { 33 | IAQScale() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Meshtastic/Enums/EthernetModes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkEnums.swift 3 | // Meshtastic 4 | // 5 | // Copyright(C) Garth Vander Houwen 11/25/22. 6 | // 7 | 8 | import Foundation 9 | import MeshtasticProtobufs 10 | 11 | enum EthernetMode: Int, CaseIterable, Identifiable { 12 | 13 | case dhcp = 0 14 | case staticip = 1 15 | 16 | var id: Int { self.rawValue } 17 | var description: String { 18 | 19 | switch self { 20 | case .dhcp: 21 | return "DHCP" 22 | case .staticip: 23 | return "Static IP" 24 | } 25 | } 26 | func protoEnumValue() -> Config.NetworkConfig.AddressMode { 27 | 28 | switch self { 29 | 30 | case .dhcp: 31 | return Config.NetworkConfig.AddressMode.dhcp 32 | case .staticip: 33 | return Config.NetworkConfig.AddressMode.static 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [garthvh] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/DistanceText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DistanceText.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/19/22. 6 | // 7 | 8 | import SwiftUI 9 | import CoreLocation 10 | import MapKit 11 | 12 | struct DistanceText: View { 13 | 14 | var meters: CLLocationDistance 15 | 16 | var body: some View { 17 | 18 | let distanceFormatter = MKDistanceFormatter() 19 | Text("\(distanceFormatter.string(fromDistance: Double(meters))) away") 20 | } 21 | } 22 | struct DistanceText_Previews: PreviewProvider { 23 | static var previews: some View { 24 | 25 | VStack { 26 | DistanceText(meters: 100) 27 | DistanceText(meters: 1000) 28 | DistanceText(meters: 10000) 29 | DistanceText(meters: 100000) 30 | DistanceText(meters: 1000000) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import OSLog 3 | import SwiftUI 4 | 5 | struct NodeAlertsButton: View { 6 | var context: NSManagedObjectContext 7 | 8 | @ObservedObject 9 | var node: NodeInfoEntity 10 | 11 | @ObservedObject 12 | var user: UserEntity 13 | 14 | var body: some View { 15 | Button { 16 | user.mute = !user.mute 17 | context.refresh(node, mergeChanges: true) 18 | do { 19 | try context.save() 20 | } catch { 21 | context.rollback() 22 | Logger.data.error("Save User Mute Error") 23 | } 24 | } label: { 25 | Label { 26 | Text(user.mute ? "Show alerts" : "Hide alerts") 27 | } icon: { 28 | Image(systemName: user.mute ? "bell.slash" : "bell") 29 | .symbolRenderingMode(.hierarchical) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/PreferenceKeys/TileHeightKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TileHeightKeys.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 9/21/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WeatherKitTilesHeightKey: PreferenceKey { 11 | static var defaultValue: CGFloat = 0 12 | 13 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 14 | // This method combines values from multiple child views if needed 15 | value = max(value, nextValue()) 16 | } 17 | } 18 | 19 | struct EnvironmentMetricsTilesHeightKey: PreferenceKey { 20 | static var defaultValue: CGFloat = 0 21 | 22 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 23 | // This method combines values from multiple child views if needed 24 | value = max(value, nextValue()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Meshtastic/Accessory/Helpers/LogRecord+StringRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogRecord+StringRepresentation.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 7/29/25. 6 | // 7 | 8 | import Foundation 9 | import MeshtasticProtobufs 10 | 11 | extension LogRecord { 12 | var stringRepresentation: String { 13 | var message = self.source.isEmpty ? self.message : "[\(self.source)] \(self.message)" 14 | switch self.level { 15 | case .debug: 16 | message = "DEBUG | \(message)" 17 | case .info: 18 | message = "INFO | \(message)" 19 | case .warning: 20 | message = "WARN | \(message)" 21 | case .error: 22 | message = "ERROR | \(message)" 23 | case .critical: 24 | message = "CRIT | \(message)" 25 | default: 26 | message = "DEBUG | \(message)" 27 | } 28 | return message 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic/Enums/TelemetryWeather.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelemetryWeather.swift 3 | // Meshtastic 4 | // 5 | // Copyright Garth Vander Houwen 2/4/23. 6 | // 7 | 8 | enum WeatherConditions: Int, CaseIterable, Identifiable { 9 | 10 | case clear = 0 11 | case cloudy = 1 12 | case frigid = 2 13 | case hot = 3 14 | case rain = 4 15 | case smoky = 5 16 | case snow = 6 17 | 18 | var id: Int { self.rawValue } 19 | var symbolName: String { 20 | switch self { 21 | 22 | case .clear: 23 | return "sparkle" 24 | case .cloudy: 25 | return "cloud" 26 | case .hot: 27 | return "sun.max.trianglebadge.exclamationmark.fill" 28 | case .rain: 29 | return "cloud.rain" 30 | case .frigid: 31 | return "thermometer.snowflake" 32 | case .smoky: 33 | return "smoke" 34 | case .snow: 35 | return "cloud.snow" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TextMessageSize: View { 4 | let maxbytes: Int 5 | let totalBytes: Int 6 | 7 | var body: some View { 8 | ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) 9 | .accessibilityLabel(NSLocalizedString("Message Size", comment: "VoiceOver label for message size")) 10 | .accessibilityValue(String(format: NSLocalizedString("Bytes Used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) 11 | .frame(width: 130) 12 | .padding(5) 13 | .font(.subheadline) 14 | .accentColor(.accentColor) 15 | } 16 | } 17 | 18 | struct TextMessageSizePreview: PreviewProvider { 19 | static var previews: some View { 20 | TextMessageSize(maxbytes: 200, totalBytes: 100) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/DisconnectNodeIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisconnectNodeIntent.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 4/2/25. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | struct DisconnectNodeIntent: AppIntent { 12 | static let title: LocalizedStringResource = "Disconnect Node" 13 | 14 | static let description: IntentDescription = "Disconnect the currently connected node" 15 | 16 | func perform() async throws -> some IntentResult { 17 | let isConnected = await AccessoryManager.shared.isConnected 18 | if !isConnected { 19 | throw AppIntentErrors.AppIntentError.notConnected 20 | } 21 | 22 | do { 23 | try await AccessoryManager.shared.disconnect() 24 | } catch { 25 | throw AppIntentErrors.AppIntentError.message("Error disconnecting node") 26 | } 27 | 28 | return .result() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic/AppState.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | class AppState: ObservableObject { 5 | 6 | @Published var router: Router 7 | @Published var unreadChannelMessages: Int 8 | @Published var unreadDirectMessages: Int 9 | 10 | var totalUnreadMessages: Int { 11 | unreadChannelMessages + unreadDirectMessages 12 | } 13 | private var cancellables: Set = [] 14 | 15 | init(router: Router) { 16 | self.router = router 17 | self.unreadChannelMessages = 0 18 | self.unreadDirectMessages = 0 19 | 20 | // Keep app icon badge count in sync with messages read status 21 | $unreadChannelMessages.combineLatest($unreadDirectMessages) 22 | .sink(receiveValue: { badgeCounts in 23 | UNUserNotificationCenter.current() 24 | .setBadgeCount(badgeCounts.0 + badgeCounts.1) 25 | }) 26 | .store(in: &cancellables) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import MeshtasticProtobufs 3 | 4 | extension StoreForwardConfigEntity { 5 | convenience init( 6 | context: NSManagedObjectContext, 7 | config: ModuleConfig.StoreForwardConfig 8 | ) { 9 | self.init(context: context) 10 | self.enabled = config.enabled 11 | self.heartbeat = config.heartbeat 12 | self.records = Int32(config.records) 13 | self.historyReturnMax = Int32(config.historyReturnMax) 14 | self.historyReturnWindow = Int32(config.historyReturnWindow) 15 | } 16 | 17 | func update(with config: ModuleConfig.StoreForwardConfig) { 18 | enabled = config.enabled 19 | heartbeat = config.heartbeat 20 | records = Int32(config.records) 21 | historyReturnMax = Int32(config.historyReturnMax) 22 | historyReturnWindow = Int32(config.historyReturnWindow) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/LocationEntityExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationEntityExtension.swift 3 | // Meshtastic 4 | // 5 | // Copyright (c) Garth Vander Houwen 11/21/23. 6 | // 7 | 8 | import CoreData 9 | import CoreLocation 10 | import MapKit 11 | import SwiftUI 12 | 13 | extension LocationEntity { 14 | 15 | var latitude: Double? { 16 | 17 | let d = Double(latitudeI) 18 | if d == 0 { 19 | return 0 20 | } 21 | return d / 1e7 22 | } 23 | 24 | var longitude: Double? { 25 | 26 | let d = Double(longitudeI) 27 | if d == 0 { 28 | return 0 29 | } 30 | return d / 1e7 31 | } 32 | 33 | var locationCoordinate: CLLocationCoordinate2D? { 34 | if latitudeI != 0 && longitudeI != 0 { 35 | let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) 36 | return coord 37 | } else { 38 | return nil 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TraceRouteEntityExtension.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 12/7/23. 6 | // 7 | 8 | import CoreData 9 | import CoreLocation 10 | import MapKit 11 | import SwiftUI 12 | 13 | extension TraceRouteHopEntity { 14 | 15 | var latitude: Double? { 16 | 17 | let d = Double(latitudeI) 18 | if d == 0 { 19 | return 0 20 | } 21 | return d / 1e7 22 | } 23 | 24 | var longitude: Double? { 25 | 26 | let d = Double(longitudeI) 27 | if d == 0 { 28 | return 0 29 | } 30 | return d / 1e7 31 | } 32 | 33 | var coordinate: CLLocationCoordinate2D? { 34 | if latitudeI != 0 && longitudeI != 0 { 35 | let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) 36 | return coord 37 | } else { 38 | return nil 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/DateTimeText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateTimeText.swift 3 | // Meshtastic Apple 4 | // 5 | // Copyright(C) Garth Vander Houwen 5/30/22. 6 | // 7 | 8 | import SwiftUI 9 | // 10 | // LastHeardText.swift 11 | // Meshtastic Apple 12 | // 13 | // Created by Garth Vander Houwen on 5/25/22. 14 | // 15 | struct DateTimeText: View { 16 | var dateTime: Date? 17 | 18 | let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) 19 | let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current) 20 | 21 | var body: some View { 22 | let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a") 23 | 24 | if dateTime != nil && dateTime! >= sixMonthsAgo! { 25 | Text(" \(dateTime!.formattedDate(format: dateFormatString))") 26 | } else { 27 | Text("Unknown Age") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic/Export/CsvDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CsvDocument.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 7/15/22. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | struct CsvDocument: FileDocument { 12 | 13 | static let readableContentTypes = [UTType.commaSeparatedText] 14 | 15 | @State var csvData: String 16 | 17 | init(emptyCsv: String = "" ) { 18 | 19 | csvData = emptyCsv 20 | } 21 | 22 | init(configuration: ReadConfiguration) throws { 23 | if let data = configuration.file.regularFileContents { 24 | csvData = String(data: data, encoding: .utf8) ?? "" 25 | 26 | } else { 27 | throw CocoaError(.fileReadCorruptFile) 28 | } 29 | } 30 | 31 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { 32 | let data = Data(csvData.utf8) 33 | return FileWrapper(regularFileWithContents: data) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import MeshtasticProtobufs 3 | 4 | extension SerialConfigEntity { 5 | convenience init( 6 | context: NSManagedObjectContext, 7 | config: ModuleConfig.SerialConfig 8 | ) { 9 | self.init(context: context) 10 | self.enabled = config.enabled 11 | self.echo = config.echo 12 | self.rxd = Int32(config.rxd) 13 | self.txd = Int32(config.txd) 14 | self.baudRate = Int32(config.baud.rawValue) 15 | self.timeout = Int32(config.timeout) 16 | self.mode = Int32(config.mode.rawValue) 17 | } 18 | 19 | func update(with config: ModuleConfig.SerialConfig) { 20 | enabled = config.enabled 21 | echo = config.echo 22 | rxd = Int32(config.rxd) 23 | txd = Int32(config.txd) 24 | baudRate = Int32(config.baud.rawValue) 25 | timeout = Int32(config.timeout) 26 | mode = Int32(config.mode.rawValue) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Inner Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Inner Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/Color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/OSLogEntryLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLogEntryLog.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 6/28/24. 6 | // 7 | 8 | import OSLog 9 | import SwiftUI 10 | 11 | /// Extensions to allow rendering of the emoji string and log leve coloring in the grid of OSLogEntryLog items 12 | extension OSLogEntryLog.Level { 13 | var description: String { 14 | switch self { 15 | case .undefined: "undefined" 16 | case .debug: "🪲 Debug" 17 | case .info: "ℹ️ Info" 18 | case .notice: "⚠️ Notice" 19 | case .error: "🚨 Error" 20 | case .fault: "💥 Fault" 21 | @unknown default: "Default".localized 22 | } 23 | } 24 | var color: Color { 25 | switch self { 26 | case .undefined: .green 27 | case .debug: .indigo 28 | case .info: .green 29 | case .notice: .orange 30 | case .error: .red 31 | case .fault: .red 32 | @unknown default: .green 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/TracerouteIntent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppIntents 3 | 4 | struct TracerouteIntent: AppIntent { 5 | static var title: LocalizedStringResource = "Send a Traceroute" 6 | 7 | static var description: IntentDescription = "Send a traceroute request to a certain Meshtastic node" 8 | 9 | @Parameter(title: "Node Number") 10 | var nodeNumber: Int 11 | 12 | static var parameterSummary: some ParameterSummary { 13 | Summary("Send traceroute to \(\.$nodeNumber)") 14 | } 15 | 16 | func perform() async throws -> some IntentResult { 17 | if !BLEManager.shared.isConnected { 18 | throw AppIntentErrors.AppIntentError.notConnected 19 | } 20 | 21 | if !BLEManager.shared.sendTraceRouteRequest(destNum: Int64(nodeNumber), wantResponse: true) { 22 | throw AppIntentErrors.AppIntentError.message("Failed to send traceroute request") 23 | } 24 | 25 | return .result() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Chirpy.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_Chirpy.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "AppIcon_Chirpy_Dark.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "idiom" : "universal", 29 | "platform" : "ios", 30 | "size" : "1024x1024" 31 | } 32 | ], 33 | "info" : { 34 | "author" : "xcode", 35 | "version" : 1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/LiveActivityBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.56", 9 | "green" : "0.96", 10 | "red" : "0.32" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.420", 27 | "green" : "0.470", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Views/Settings/AppIconPicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppIconPicker: View { 4 | private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } 5 | @Environment(\.managedObjectContext) var context 6 | @Binding var isPresenting: Bool 7 | @State private var didError = false 8 | @State private var errorDetails: String? 9 | var iconNames: [String?: String] = [nil: "Default", "AppIcon_MPowered": "Meshtastic Powered", "AppIcon_Chirpy": "Chirpy", "AppIcon_Ham": "Ham"] 10 | 11 | // MARK: View 12 | var body: some View { 13 | List { 14 | Section(header: Text("Icons")) { 15 | ForEach(Array(iconNames.enumerated()), id: \.offset) { _, icon in 16 | AppIconButton(iconDescription: .constant(icon.value), iconName: .constant(icon.key), isPresenting: $isPresenting) 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | #Preview{ 24 | AppIconPicker(isPresenting: .constant(true)) 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-3.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "logo-dark.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "logo-tinted.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Enums/ChannelRoles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelRoles.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/21/22. 6 | // 7 | import Foundation 8 | import MeshtasticProtobufs 9 | 10 | // Default of 0 is Client 11 | enum ChannelRoles: Int, CaseIterable, Identifiable { 12 | 13 | case disabled = 0 14 | case primary = 1 15 | case secondary = 2 16 | 17 | var id: Int { self.rawValue } 18 | var description: String { 19 | switch self { 20 | 21 | case .disabled: 22 | return "Disabled".localized 23 | case .primary: 24 | return "Primary".localized 25 | case .secondary: 26 | return "Secondary".localized 27 | } 28 | } 29 | func protoEnumValue() -> Channel.Role { 30 | 31 | switch self { 32 | 33 | case .disabled: 34 | return Channel.Role.disabled 35 | case .primary: 36 | return Channel.Role.primary 37 | case .secondary: 38 | return Channel.Role.secondary 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Meshtastic/Resources/Assets.xcassets/Assets.xcassets/Color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIconDebug.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "DevIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "DevIcon-Dark.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "DevIcon-Tinted.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "meshtastic_ham.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "logo-dark.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "logo-dark 1.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MPowered.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "MPowered 1.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "MPowered 2.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Tips/PersistantTips.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Untitled.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 6/19/25. 6 | // 7 | import TipKit 8 | 9 | struct PersistentTip: TipViewStyle { 10 | func makeBody(configuration: Configuration) -> some View { 11 | VStack { 12 | HStack(alignment: .top) { 13 | if let image = configuration.image { 14 | image 15 | .font(.system(size: 42)) 16 | .foregroundColor(.accentColor) 17 | .padding(.trailing, 5) 18 | } 19 | VStack(alignment: .leading) { 20 | if let title = configuration.title { 21 | title 22 | .bold() 23 | .font(.headline) 24 | } 25 | if let message = configuration.message { 26 | message 27 | .foregroundStyle(.secondary) 28 | .font(.callout) 29 | } 30 | } 31 | } 32 | } 33 | .frame(maxWidth: .infinity) 34 | .backgroundStyle(.thinMaterial) 35 | .padding() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon_Ham.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "automatic-gradient" : "display-p3:0.54507,0.90596,0.61177,1.00000" 4 | }, 5 | "groups" : [ 6 | { 7 | "layers" : [ 8 | { 9 | "blend-mode" : "normal", 10 | "glass" : false, 11 | "hidden" : false, 12 | "image-name" : "ham.png", 13 | "name" : "ham", 14 | "position" : { 15 | "scale" : 1.65, 16 | "translation-in-points" : [ 17 | -2.0625, 18 | 7.703125 19 | ] 20 | } 21 | } 22 | ], 23 | "shadow" : { 24 | "kind" : "neutral", 25 | "opacity" : 0.5 26 | }, 27 | "translucency" : { 28 | "enabled" : true, 29 | "value" : 0.5 30 | } 31 | } 32 | ], 33 | "supported-platforms" : { 34 | "circles" : [ 35 | "watchOS" 36 | ], 37 | "squares" : "shared" 38 | } 39 | } -------------------------------------------------------------------------------- /Meshtastic/Extensions/UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIImage { 12 | func rotate(radians: Float) -> UIImage? { 13 | var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size 14 | newSize.width = floor(newSize.width) 15 | newSize.height = floor(newSize.height) 16 | UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) 17 | let context = UIGraphicsGetCurrentContext()! 18 | context.translateBy(x: newSize.width/2, y: newSize.height/2) 19 | context.rotate(by: CGFloat(radians)) 20 | self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height)) 21 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 22 | UIGraphicsEndImageContext() 23 | 24 | return newImage 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Messages/MessageTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageTemplate.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/18/22. 6 | // 7 | import SwiftUI 8 | 9 | struct MessageTemplate: View { 10 | 11 | var user: UserEntity 12 | var message: MessageEntity 13 | var messageReply: MessageEntity? 14 | 15 | var body: some View { 16 | 17 | // Display the message being replied to and the arrow 18 | if message.replyID > 0 { 19 | 20 | HStack { 21 | 22 | Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) 23 | .padding(10) 24 | .overlay( 25 | RoundedRectangle(cornerRadius: 18) 26 | .stroke(Color.blue, lineWidth: 0.5) 27 | ) 28 | Image(systemName: "arrowshape.turn.up.left.fill") 29 | .symbolRenderingMode(.hierarchical) 30 | .imageScale(.large).foregroundColor(.blue) 31 | .padding(.trailing) 32 | } 33 | } 34 | 35 | // Message 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Enums/BluetoothModes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothModes.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/19/22. 6 | // 7 | import Foundation 8 | import MeshtasticProtobufs 9 | 10 | enum BluetoothModes: Int, CaseIterable, Identifiable { 11 | 12 | case randomPin = 0 13 | case fixedPin = 1 14 | case noPin = 2 15 | 16 | var id: Int { self.rawValue } 17 | var description: String { 18 | switch self { 19 | case .randomPin: 20 | return "Random Pin".localized 21 | case .fixedPin: 22 | return "Fixed Pin".localized 23 | case .noPin: 24 | return "No PIN (Just Works)".localized 25 | } 26 | } 27 | func protoEnumValue() -> Config.BluetoothConfig.PairingMode { 28 | switch self { 29 | case .randomPin: 30 | return Config.BluetoothConfig.PairingMode.randomPin 31 | case .fixedPin: 32 | return Config.BluetoothConfig.PairingMode.fixedPin 33 | case .noPin: 34 | return Config.BluetoothConfig.PairingMode.noPin 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What changed? 2 | 3 | 4 | ## Why did it change? 5 | 6 | 7 | ## How is this tested? 8 | 9 | 10 | ## Screenshots/Videos (when applicable) 11 | 12 | 13 | 14 | ## Checklist 15 | 16 | - [ ] My code adheres to the project's coding and style guidelines. 17 | - [ ] I have conducted a self-review of my code. 18 | - [ ] I have commented my code, particularly in complex areas. 19 | - [ ] I have verified whether these changes require an update to existing documentation or if new documentation is needed, and created an issue in the [docs repo](http://github.com/meshtastic/meshtastic/issues) if applicable. 20 | - [ ] I have tested the change to ensure that it works as intended. 21 | 22 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CLLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocation.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/4/24. 6 | // 7 | import Foundation 8 | import MapKit 9 | 10 | func degreesToRadians(degrees: Double) -> Double { return degrees * .pi / 180.0 } 11 | func radiansToDegrees(radians: Double) -> Double { return radians * 180.0 / .pi } 12 | 13 | func getBearingBetweenTwoPoints(point1: CLLocation, point2: CLLocation) -> Double { 14 | 15 | let lat1 = degreesToRadians(degrees: point1.coordinate.latitude) 16 | let lon1 = degreesToRadians(degrees: point1.coordinate.longitude) 17 | 18 | let lat2 = degreesToRadians(degrees: point2.coordinate.latitude) 19 | let lon2 = degreesToRadians(degrees: point2.coordinate.longitude) 20 | 21 | let dLon = lon2 - lon1 22 | 23 | let y = sin(dLon) * cos(lat2) 24 | let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) 25 | let radiansBearing = atan2(y, x) 26 | 27 | return radiansToDegrees(radians: radiansBearing) 28 | } 29 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Middle Stroke 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Outer Stroke 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Middle Stroke 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Outer Stroke 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Middle Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Outer Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Middle Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Outer Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Help/AckErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAQScale.swift 3 | // Meshtastic 4 | // 5 | // Copyright Garth Vander Houwen 4/24/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AckErrors: View { 11 | 12 | var body: some View { 13 | VStack(alignment: .leading) { 14 | Text("Message Status Options") 15 | .font(.title2) 16 | HStack { 17 | RoundedRectangle(cornerRadius: 5) 18 | .fill(.orange) 19 | .frame(width: 20, height: 12) 20 | Text("Acknowledged by another node") 21 | .font(.caption) 22 | .foregroundStyle(.orange) 23 | } 24 | ForEach(RoutingError.allCases) { re in 25 | HStack { 26 | RoundedRectangle(cornerRadius: 5) 27 | .fill(re.color) 28 | .frame(width: 20, height: 12) 29 | Text(re.display) 30 | .font(.caption) 31 | .foregroundStyle(re.color) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | struct AckErrorsPreviews: PreviewProvider { 39 | static var previews: some View { 40 | VStack { 41 | AckErrors() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | import MeshtasticProtobufs 4 | 5 | extension DeviceMetadataEntity { 6 | convenience init( 7 | context: NSManagedObjectContext, 8 | metadata: DeviceMetadata 9 | ) { 10 | self.init(context: context) 11 | self.time = Date() 12 | self.deviceStateVersion = Int32(metadata.deviceStateVersion) 13 | self.canShutdown = metadata.canShutdown 14 | self.hasWifi = metadata.hasWifi_p 15 | self.hasBluetooth = metadata.hasBluetooth_p 16 | self.hasEthernet = metadata.hasEthernet_p 17 | self.role = Int32(metadata.role.rawValue) 18 | self.positionFlags = Int32(metadata.positionFlags) 19 | // Swift does strings weird, this does work to get the version without the github hash 20 | let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") 21 | var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] 22 | version = version.dropLast() 23 | self.firmwareVersion = String(version) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/WeightCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeightCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WeightCompactWidget: View { 11 | let weight: String 12 | let unit: String 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | HStack(alignment: .firstTextBaseline) { 17 | Image(systemName: "scalemass") 18 | .imageScale(.small) 19 | .foregroundColor(.accentColor) 20 | Text("Weight") 21 | .textCase(.uppercase) 22 | .font(.callout) 23 | } 24 | HStack { 25 | Text("\(weight)") 26 | .font(weight.length < 4 ? .system(size: 50) : .system(size: 40) ) 27 | Text(unit) 28 | .font(.system(size: 14)) 29 | } 30 | } 31 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 32 | .padding() 33 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 34 | } 35 | } 36 | 37 | #Preview { 38 | WeightCompactWidget(weight: "123", unit: "kg") 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic/Accessory/Protocols/Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 7/10/25. 6 | // 7 | 8 | import Foundation 9 | import MeshtasticProtobufs 10 | 11 | protocol Connection: Actor { 12 | var type: TransportType { get } 13 | 14 | var isConnected: Bool { get } 15 | func send(_ data: ToRadio) async throws 16 | func connect() async throws -> AsyncStream 17 | func disconnect(withError: Error?, shouldReconnect: Bool) throws 18 | func drainPendingPackets() async throws 19 | func startDrainPendingPackets() throws 20 | 21 | func appDidEnterBackground() 22 | func appDidBecomeActive() 23 | } 24 | 25 | enum ConnectionEvent { 26 | case data(FromRadio) 27 | case logMessage(String) 28 | case rssiUpdate(Int) 29 | case error(Error) 30 | case errorWithoutReconnect(Error) 31 | case disconnected(shouldReconnect: Bool) 32 | } 33 | 34 | enum ConnectionState: Equatable, Codable { 35 | case disconnected 36 | case connecting 37 | case connected 38 | } 39 | 40 | enum ConnectionError: Error, LocalizedError { 41 | case ioError(String) 42 | } 43 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-black.imageset/Mesh_Logo_Black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/DistanceCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DistanceCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DistanceCompactWidget: View { 11 | let distance: String 12 | let unit: String 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | HStack(alignment: .firstTextBaseline) { 17 | Image(systemName: "ruler") 18 | .imageScale(.small) 19 | .foregroundColor(.accentColor) 20 | Text("Distance") 21 | .textCase(.uppercase) 22 | .font(.callout) 23 | } 24 | HStack { 25 | Text("\(distance)") 26 | .font(distance.length < 4 ? .system(size: 50) : .system(size: 40) ) 27 | Text(unit) 28 | .font(.system(size: 14)) 29 | } 30 | } 31 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 32 | .padding() 33 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 34 | } 35 | } 36 | 37 | #Preview { 38 | DistanceCompactWidget(distance: "123", unit: "mm") 39 | } 40 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/RadiationCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadiationCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RadiationCompactWidget: View { 11 | let radiation: String 12 | let unit: String 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | HStack(alignment: .firstTextBaseline) { 17 | Text(verbatim: "☢") 18 | .font(.system(size: 30, design: .monospaced)) 19 | .tint(.accentColor) 20 | Text("Radiation") 21 | .textCase(.uppercase) 22 | .font(.callout) 23 | } 24 | HStack { 25 | Text("\(radiation)") 26 | .font(radiation.length < 4 ? .system(size: 50) : .system(size: 34) ) 27 | Text(unit) 28 | .font(.system(size: 14)) 29 | } 30 | } 31 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 32 | .padding() 33 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 34 | } 35 | } 36 | 37 | #Preview { 38 | RadiationCompactWidget(radiation: "15", unit: "µR/hr") 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/ChannelLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelLock.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 6/22/25. 6 | // 7 | import SwiftUI 8 | 9 | struct ChannelLock: View { 10 | 11 | @ObservedObject var channel: ChannelEntity 12 | var body: some View { 13 | /// Unencrypted - using no key at all or a known 1 byte key 14 | if channel.psk?.hexDescription.count ?? 0 < 3 { 15 | let preciseLoction = 17...32 16 | // Using precise location and have MQTT uplink enabled 17 | if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { 18 | Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") 19 | .foregroundColor(.red) 20 | // Using precise location 21 | } else if preciseLoction ~= (Int(channel.positionPrecision)) { 22 | Image(systemName: "lock.open.fill") 23 | .foregroundColor(.red) 24 | // Just unencrypted without any location or MQTT 25 | } else { 26 | Image(systemName: "lock.open.fill") 27 | .foregroundColor(.yellow) 28 | } 29 | } else { 30 | Image(systemName: "lock.fill") 31 | .foregroundColor(.green) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import OSLog 3 | struct ClientHistoryButton: View { 4 | @EnvironmentObject var accessoryManager: AccessoryManager 5 | 6 | var connectedNode: NodeInfoEntity 7 | 8 | var node: NodeInfoEntity 9 | 10 | @State 11 | private var isPresentingAlert = false 12 | 13 | var body: some View { 14 | Button { 15 | Task { 16 | do { 17 | try await accessoryManager.requestStoreAndForwardClientHistory( 18 | fromUser: connectedNode.user!, 19 | toUser: node.user! 20 | ) 21 | Task { @MainActor in 22 | isPresentingAlert = true 23 | } 24 | } catch { 25 | Logger.mesh.warning("Failed to send client history request: \(error)") 26 | } 27 | } 28 | } label: { 29 | Label( 30 | "Client History", 31 | systemImage: "envelope.arrow.triangle.branch" 32 | ) 33 | }.alert( 34 | "Client History Request Sent", 35 | isPresented: $isPresentingAlert 36 | ) { 37 | Button("OK") { }.keyboardShortcut(.defaultAction) 38 | } message: { 39 | Text("Any missed messages will be delivered again.") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Widgets/MeshActivityAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshActivityAttributes.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 3/1/23. 6 | // 7 | #if !targetEnvironment(macCatalyst) 8 | #if canImport(ActivityKit) 9 | 10 | import ActivityKit 11 | import WidgetKit 12 | import SwiftUI 13 | 14 | struct MeshActivityAttributes: ActivityAttributes { 15 | public typealias MeshActivityStatus = ContentState 16 | public struct ContentState: Codable, Hashable { 17 | // Dynamic stateful properties about your activity go here! 18 | var uptimeSeconds: UInt32? 19 | var channelUtilization: Float? 20 | var airtime: Float? 21 | var sentPackets: UInt32 22 | var receivedPackets: UInt32 23 | var badReceivedPackets: UInt32 24 | var dupeReceivedPackets: UInt32 25 | var packetsSentRelay: UInt32 26 | var packetsCanceledRelay: UInt32 27 | var nodesOnline: UInt32 28 | var totalNodes: UInt32 29 | 30 | public var numTxRelayCanceled: UInt32 = 0 31 | var timerRange: ClosedRange 32 | } 33 | 34 | // Fixed non-changing properties about your activity go here! 35 | var nodeNum: Int 36 | var name: String 37 | } 38 | #endif 39 | #endif 40 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-white.imageset/Mesh_Logo_White.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | var lastHeard: String { 13 | if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { 14 | formatted() 15 | } else { 16 | "Unknown Age".localized 17 | } 18 | } 19 | 20 | func formattedDate(format: String) -> String { 21 | let dateformat = DateFormatter() 22 | dateformat.dateFormat = format 23 | if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { 24 | return dateformat.string(from: self) 25 | } else { 26 | return "Unknown Age".localized 27 | } 28 | } 29 | func relativeTimeOfDay() -> String { 30 | let hour = Calendar.current.component(.hour, from: self) 31 | 32 | switch hour { 33 | case 6..<12: return "Morning".localized 34 | case 12: return "Midday".localized 35 | case 13..<17: return "Afternoon".localized 36 | case 17..<22: return "Evening".localized 37 | default: return "Nighttime".localized 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Meshtastic Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Meshtastic Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // Meshtastic 4 | // 5 | // Created by Garth Vander Houwen on 12/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | public var appName: String { getInfo("CFBundleName") } 12 | public var displayName: String { getInfo("CFBundleDisplayName") } 13 | public var language: String { getInfo("CFBundleDevelopmentRegion") } 14 | public var identifier: String { getInfo("CFBundleIdentifier") } 15 | public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") } 16 | 17 | public var appBuild: String { getInfo("CFBundleVersion") } 18 | public var appVersionLong: String { getInfo("CFBundleShortVersionString") } 19 | // public var appVersionShort: String { getInfo("CFBundleShortVersion") } 20 | 21 | fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } 22 | 23 | public var isTestFlight: Bool { 24 | return appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" 25 | } 26 | 27 | public var isDebug: Bool { 28 | #if DEBUG 29 | return true 30 | #else 31 | return false 32 | #endif 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon.icon/Assets/Icon Grid Inner Stroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIconDebug.icon/Assets/Icon Grid Inner Stroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic/Meshtastic.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | applinks:meshtastic.org/e/* 8 | applinks:meshtastic.org/v/* 9 | 10 | com.apple.developer.carplay-communication 11 | 12 | com.apple.developer.usernotifications.critical-alerts 13 | 14 | com.apple.developer.weatherkit 15 | 16 | com.apple.security.app-sandbox 17 | 18 | com.apple.security.device.bluetooth 19 | 20 | com.apple.security.device.serial 21 | 22 | com.apple.security.device.usb 23 | 24 | com.apple.security.files.user-selected.read-write 25 | 26 | com.apple.security.network.client 27 | 28 | com.apple.security.personal-information.location 29 | 30 | keychain-access-groups 31 | 32 | $(AppIdentifierPrefix)gvh.MeshtasticClient 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/CurrentConditionsCompact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentConditionsCompact.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 2/5/23. 6 | // 7 | import SwiftUI 8 | 9 | struct CurrentConditionsCompact: View { 10 | var temp: Float 11 | var condition: WeatherConditions 12 | 13 | var body: some View { 14 | Label("\(String(temp.formattedTemperature()))", systemImage: condition.symbolName) 15 | .font(.caption) 16 | .foregroundColor(.gray) 17 | .symbolRenderingMode(.multicolor) 18 | } 19 | } 20 | 21 | struct CurrentConditionsCompact_Previews: PreviewProvider { 22 | static var previews: some View { 23 | 24 | VStack { 25 | CurrentConditionsCompact(temp: 22, condition: WeatherConditions.clear) 26 | CurrentConditionsCompact(temp: 17, condition: WeatherConditions.cloudy) 27 | CurrentConditionsCompact(temp: -5, condition: WeatherConditions.frigid) 28 | CurrentConditionsCompact(temp: 38, condition: WeatherConditions.hot) 29 | CurrentConditionsCompact(temp: 10, condition: WeatherConditions.rain) 30 | CurrentConditionsCompact(temp: 30, condition: WeatherConditions.smoky) 31 | CurrentConditionsCompact(temp: -2, condition: WeatherConditions.snow) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/Float.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Float.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Float { 11 | 12 | func formattedTemperature() -> String { 13 | let temperature = Measurement(value: Double(self), unit: .celsius) 14 | let mf = MeasurementFormatter() 15 | mf.numberFormatter.maximumFractionDigits = 0 16 | return mf.string(from: temperature) 17 | } 18 | func shortFormattedTemperature() -> String { 19 | let temperature = Measurement(value: Double(self), unit: .celsius) 20 | let mf = MeasurementFormatter() 21 | mf.unitStyle = .short 22 | mf.numberFormatter.maximumFractionDigits = 0 23 | return mf.string(from: temperature) 24 | } 25 | func localeTemperature() -> Double { 26 | let temperature = Measurement(value: Double(self), unit: .celsius) 27 | let locale = NSLocale.current as NSLocale 28 | let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) 29 | var format: UnitTemperature = .celsius 30 | 31 | if localeUnit! as? String == "Fahrenheit" { 32 | format = .fahrenheit 33 | } 34 | return temperature.converted(to: format).value 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/create-release-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the release version number is provided 4 | if [ -z "$1" ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # Set the release version number 10 | RELEASE_VERSION=$1 11 | 12 | # Check if the release branch already exists on the remote repository 13 | if git ls-remote --exit-code --heads origin $RELEASE_BRANCH; then 14 | echo "The branch $RELEASE_BRANCH already exists on the remote repository." 15 | exit 1 16 | fi 17 | 18 | # Prompt the user for confirmation 19 | echo "You are about to create and push the release branch ${RELEASE_VERSION}-release." 20 | read -p "Are you sure you want to proceed? (Y/n): " confirmation 21 | 22 | # Check the user's response 23 | if [[ ! "$confirmation" =~ ^[Yy]$ ]]; then 24 | echo "Operation cancelled." 25 | exit 0 26 | fi 27 | 28 | # Check out the main branch and pull the latest changes 29 | git checkout main 30 | git pull origin main 31 | 32 | # Create a new branch for the release 33 | RELEASE_BRANCH="${RELEASE_VERSION}-release" 34 | git checkout -b $RELEASE_BRANCH 35 | 36 | # Push the new release branch to the remote repository 37 | git push origin $RELEASE_BRANCH 38 | 39 | echo "Release branch $RELEASE_BRANCH created and pushed successfully." 40 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/31/23. 6 | // 7 | import Foundation 8 | import Swift 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | private func makeColor(componentDelta: CGFloat) -> UIColor { 14 | var red: CGFloat = 0 15 | var blue: CGFloat = 0 16 | var green: CGFloat = 0 17 | var alpha: CGFloat = 0 18 | 19 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 20 | 21 | return UIColor( 22 | red: add(componentDelta, toComponent: red), 23 | green: add(componentDelta, toComponent: green), 24 | blue: add(componentDelta, toComponent: blue), 25 | alpha: alpha 26 | ) 27 | } 28 | 29 | func lighter(componentDelta: CGFloat = 0.1) -> UIColor { 30 | return makeColor(componentDelta: componentDelta) 31 | } 32 | 33 | func darker(componentDelta: CGFloat = 0.1) -> UIColor { 34 | return makeColor(componentDelta: -1*componentDelta) 35 | } 36 | 37 | private func add(_ value: CGFloat, toComponent: CGFloat) -> CGFloat { 38 | return max(0, min(1, toComponent + value)) 39 | } 40 | 41 | static var random: UIColor { 42 | return UIColor( 43 | red: .random(in: 0...1), 44 | green: .random(in: 0...1), 45 | blue: .random(in: 0...1), 46 | alpha: 1.0 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Meshtastic/Resources/AppIcon_Chirpy.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "automatic-gradient" : "extended-gray:1.00000,1.00000" 4 | }, 5 | "groups" : [ 6 | { 7 | "layers" : [ 8 | { 9 | "image-name" : "chirpy-2.svg", 10 | "name" : "chirpy-2", 11 | "position-specializations" : [ 12 | { 13 | "idiom" : "square", 14 | "value" : { 15 | "scale" : 0.48, 16 | "translation-in-points" : [ 17 | -18.390625, 18 | 127.2421875 19 | ] 20 | } 21 | }, 22 | { 23 | "idiom" : "watchOS", 24 | "value" : { 25 | "scale" : 0.52, 26 | "translation-in-points" : [ 27 | -7, 28 | 145.9296875 29 | ] 30 | } 31 | } 32 | ] 33 | } 34 | ], 35 | "shadow" : { 36 | "kind" : "neutral", 37 | "opacity" : 0.5 38 | }, 39 | "translucency" : { 40 | "enabled" : true, 41 | "value" : 0.5 42 | } 43 | } 44 | ], 45 | "supported-platforms" : { 46 | "circles" : [ 47 | "watchOS" 48 | ], 49 | "squares" : "shared" 50 | } 51 | } -------------------------------------------------------------------------------- /Meshtastic/Enums/RouteEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteEnums.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 4/14/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum ActivityType: Int, CaseIterable, Identifiable { 12 | case walking = 0 13 | case hiking = 1 14 | case biking = 2 15 | case driving = 3 16 | case overlanding = 4 17 | case skiing = 5 18 | 19 | var id: Int { self.rawValue } 20 | var description: String { 21 | switch self { 22 | case .walking: 23 | return "Walking".localized 24 | case .hiking: 25 | return "Hiking".localized 26 | case .biking: 27 | return "Biking".localized 28 | case .driving: 29 | return "Driving".localized 30 | case .overlanding: 31 | return "Overlanding".localized 32 | case .skiing: 33 | return "Skiing".localized 34 | } 35 | } 36 | 37 | var fileNameString: String { 38 | switch self { 39 | case .walking: 40 | return "Walking".localized.lowercased() 41 | case .hiking: 42 | return "Hiking".localized.lowercased() 43 | case .biking: 44 | return "Biking".localized.lowercased() 45 | case .driving: 46 | return "Driving".localized.lowercased() 47 | case .overlanding: 48 | return "Overlanding".localized.lowercased() 49 | case .skiing: 50 | return "Skiing".localized.lowercased() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Meshtastic/Enums/KeyBackupStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iCloudStats.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 6/18/25. 6 | // 7 | 8 | enum KeyBackupStatus: String, CaseIterable, Equatable, Decodable { 9 | case saved 10 | case restored 11 | case deleted 12 | case saveFailed 13 | case restoreFailed 14 | case deleteFailed 15 | var description: String { 16 | switch self { 17 | case .saved: 18 | return "Private Key saved successfully to iCloud keychain.".localized 19 | case .restored: 20 | return "Private Key restored successfully from iCloud keychain.".localized 21 | case .deleted: 22 | return "Private Key deleted successfully from iCloud keychain.".localized 23 | case .saveFailed: 24 | return "Private Key failed to save to iCloud keychain.".localized 25 | case .restoreFailed: 26 | return "Private Key value not found in iCloud keychain.".localized 27 | case .deleteFailed: 28 | return "Private Key failed to delete from iCloud keychain.".localized 29 | } 30 | } 31 | var success: Bool { 32 | switch self { 33 | case .saved: 34 | return true 35 | case .restored: 36 | return true 37 | case .deleted: 38 | return true 39 | case .saveFailed: 40 | return false 41 | case .restoreFailed: 42 | return false 43 | case .deleteFailed: 44 | return false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/PressureCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PressureCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | import SwiftUI 8 | 9 | struct PressureCompactWidget: View { 10 | let pressure: String 11 | let unit: String 12 | let low: Bool 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | HStack(spacing: 5.0) { 16 | Image(systemName: "gauge") 17 | .foregroundColor(.accentColor) 18 | .font(.callout) 19 | Text("Pressure") 20 | .textCase(.uppercase) 21 | .font(.caption) 22 | } 23 | Text(pressure) 24 | .font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) ) 25 | Text(low ? "LOW" : "HIGH") 26 | .padding(.bottom, 10) 27 | Text(unit) 28 | } 29 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 30 | .padding() 31 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 32 | } 33 | } 34 | 35 | #Preview { 36 | let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) 37 | Form { 38 | LazyVGrid(columns: gridItemLayout) { 39 | PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: true) 40 | PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: false) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Meshtastic/Model/CoreData/TelemetryEntity+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelemetryEntity+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Jake Bordens on 12/26/24. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | // Manual implementation of the TelemetryEntry object for CoreData. 13 | // Add non-optional scalar types here using the standard @NSManaged proprty wrapper 14 | // Add optional/non-optional object types here using the standard @NSManaged proprty wrapper 15 | // CoreData is based on Objective-C which natively supports optionals for class types and 16 | // non-optional scalars. 17 | 18 | extension TelemetryEntity { 19 | 20 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 21 | return NSFetchRequest(entityName: "TelemetryEntity") 22 | } 23 | 24 | @NSManaged public var time: Date? 25 | @NSManaged public var metricsType: Int32 26 | @NSManaged public var numOnlineNodes: Int32 27 | @NSManaged public var numPacketsRx: Int32 28 | @NSManaged public var numPacketsRxBad: Int32 29 | @NSManaged public var numPacketsTx: Int32 30 | @NSManaged public var numRxDupe: Int32 31 | @NSManaged public var numTotalNodes: Int32 32 | @NSManaged public var numTxRelay: Int32 33 | @NSManaged public var numTxRelayCanceled: Int32 34 | @NSManaged public var nodeTelemetry: NodeInfoEntity? 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/RestartNodeIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestartNodeIntent.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 8/24/24. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | struct RestartNodeIntent: AppIntent { 12 | static let title: LocalizedStringResource = "Restart" 13 | 14 | static let description: IntentDescription = "Restart to the node you are connected to" 15 | 16 | func perform() async throws -> some IntentResult { 17 | 18 | if !(await AccessoryManager.shared.isConnected) { 19 | throw AppIntentErrors.AppIntentError.notConnected 20 | } 21 | // Safely unwrap the connectedNode using if let 22 | if let connectedPeripheralNum = await AccessoryManager.shared.activeDeviceNum, 23 | let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), 24 | let fromUser = connectedNode.user, 25 | let toUser = connectedNode.user { 26 | 27 | // Attempt to send shutdown, throw an error if it fails 28 | do { 29 | try await AccessoryManager.shared.sendReboot(fromUser: fromUser, toUser: toUser) 30 | } catch { 31 | throw AppIntentErrors.AppIntentError.message("Failed to restart") 32 | } 33 | } else { 34 | throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") 35 | } 36 | return .result() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/WeatherConditionsCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherConditionsCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | import SwiftUI 8 | 9 | struct WeatherConditionsCompactWidget: View { 10 | let temperature: String 11 | let symbolName: String 12 | let description: String 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | HStack(spacing: 5.0) { 16 | Image(systemName: symbolName) 17 | .foregroundColor(.accentColor) 18 | .font(.callout) 19 | Text(description) 20 | .lineLimit(2) 21 | .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) 22 | .fixedSize(horizontal: false, vertical: true) 23 | .font(.caption) 24 | } 25 | Text(temperature) 26 | .font(temperature.length < 4 ? .system(size: 72) : .system(size: 54) ) 27 | } 28 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 29 | .padding() 30 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 31 | } 32 | } 33 | 34 | #Preview { 35 | let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) 36 | Form { 37 | LazyVGrid(columns: gridItemLayout) { 38 | WeatherConditionsCompactWidget(temperature: "24°F", symbolName: "sun.rain.fill", description: "Raining") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/NodeFilterParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeListFilterParameters.swift 3 | // Meshtastic 4 | // 5 | // Created by jake on 9/4/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor 11 | final class NodeFilterParameters: ObservableObject { 12 | // Public variables 13 | @Published var searchText = "" 14 | @Published var isOnline = false 15 | @Published var isPkiEncrypted = false 16 | @Published var isFavorite = false 17 | @Published var isIgnored = false 18 | @Published var isEnvironment = false 19 | @Published var distanceFilter = false 20 | @Published var maxDistance: Double = 800_000 21 | @Published var hopsAway: Double = -1.0 22 | @Published var roleFilter = false 23 | @Published var deviceRoles: Set = [] 24 | 25 | // Private backing vars 26 | @Published private var _viaLora = true 27 | @Published private var _viaMqtt = true 28 | 29 | // Public computed wrappers with enforcement 30 | var viaLora: Bool { 31 | get { _viaLora } 32 | set { 33 | objectWillChange.send() 34 | _viaLora = newValue 35 | if !_viaLora && !_viaMqtt { 36 | _viaMqtt = true // enforce at least one ON 37 | } 38 | } 39 | } 40 | 41 | var viaMqtt: Bool { 42 | get { _viaMqtt } 43 | set { 44 | objectWillChange.send() 45 | _viaMqtt = newValue 46 | if !_viaLora && !_viaMqtt { 47 | _viaLora = true // enforce at least one ON 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import OSLog 3 | struct TraceRouteButton: View { 4 | @EnvironmentObject var accessoryManager: AccessoryManager 5 | 6 | var node: NodeInfoEntity 7 | 8 | @State 9 | private var isPresentingTraceRouteSentAlert: Bool = false 10 | 11 | var body: some View { 12 | RateLimitedButton(key: "traceroute", rateLimit: 30.0) { 13 | Task { 14 | do { 15 | try await accessoryManager.sendTraceRouteRequest( 16 | destNum: node.user?.num ?? 0, 17 | wantResponse: true 18 | ) 19 | Task { 20 | isPresentingTraceRouteSentAlert = true 21 | } 22 | } catch { 23 | Logger.mesh.warning("Failed to send traceroute request: \(error)") 24 | } 25 | } 26 | } label: { completion in 27 | if let completion, completion.percentComplete > 0.0 { 28 | Label { 29 | Text("Trace Route (in \(completion.secondsRemaining.formatted(.number.precision(.fractionLength(0))))s)") 30 | .foregroundStyle(.secondary) 31 | } icon: { 32 | Image("progress.ring.dashed", variableValue: completion.percentComplete) 33 | .foregroundStyle(.secondary) 34 | }.disabled(true) 35 | } else { 36 | Label { 37 | Text("Trace Route") 38 | } icon: { 39 | Image(systemName: "signpost.right.and.left") 40 | .symbolRenderingMode(.hierarchical) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meshtastic Apple Clients 2 | 3 | ## Overview 4 | 5 | SwiftUI client applications for iOS, iPadOS and macOS. 6 | 7 | ## Getting Started 8 | 9 | This project always uses the latest release version of XCode. 10 | 11 | 1. Clone the repo. 12 | ```sh 13 | git clone git@github.com:meshtastic/Meshtastic-Apple.git 14 | ``` 15 | 2. Open the local directory. 16 | ```sh 17 | cd Meshtastic-Apple 18 | ``` 19 | 3. Set up git hooks to automatically lint the project when you commit changes. 20 | ```sh 21 | ./scripts/setup-hooks.sh 22 | ``` 23 | 4. Open `Meshtastic.xcworkspace` 24 | ```sh 25 | open Meshtastic.xcworkspace 26 | ``` 27 | 5. Build and run the `Meshtastic` target. 28 | 29 | ## Technical Standards 30 | 31 | ### Supported Operating Systems 32 | 33 | The last two major operating system versions are supported on iOS, iPadOS and macOS. 34 | 35 | ### Code Standards 36 | 37 | - Use SwiftUI 38 | - Use SFSymbols for icons 39 | - Use Core Data for persistence 40 | 41 | ## Updating Protobufs: 42 | 43 | 1. run 44 | ```bash 45 | ./scripts/gen_protos.sh 46 | ``` 47 | 2. Build, test, and commit the changes. 48 | 49 | ## Release Process 50 | 51 | For more information on how a new release of Meshtastic is managed, please refer to [RELEASING.md](./RELEASING.md) 52 | 53 | ## License 54 | 55 | This project is licensed under the GPL v3. See the [LICENSE](LICENSE) file for details. 56 | -------------------------------------------------------------------------------- /Meshtastic/Enums/MessagingEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagingEnums.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/30/22. 6 | // 7 | import Foundation 8 | 9 | enum BubblePosition { 10 | case left 11 | case right 12 | } 13 | 14 | enum Tapbacks: Int, CaseIterable, Identifiable { 15 | 16 | case wave = 0 17 | case heart = 1 18 | case thumbsUp = 2 19 | case thumbsDown = 3 20 | case haHa = 4 21 | case exclamation = 5 22 | case question = 6 23 | case poop = 7 24 | 25 | var id: Int { self.rawValue } 26 | var emojiString: String { 27 | switch self { 28 | case .wave: 29 | return "👋" 30 | case .heart: 31 | return "❤️" 32 | case .thumbsUp: 33 | return "👍" 34 | case .thumbsDown: 35 | return "👎" 36 | case .haHa: 37 | return "🤣" 38 | case .exclamation: 39 | return "‼️" 40 | case .question: 41 | return "❓" 42 | case .poop: 43 | return "💩" 44 | } 45 | } 46 | var description: String { 47 | switch self { 48 | case .wave: 49 | return "Wave".localized 50 | case .heart: 51 | return "Heart".localized 52 | case .thumbsUp: 53 | return "Thumbs Up".localized 54 | case .thumbsDown: 55 | return "Thumbs Down".localized 56 | case .haHa: 57 | return "HaHa".localized 58 | case .exclamation: 59 | return "Exclamation".localized 60 | case .question: 61 | return "Question".localized 62 | case .poop: 63 | return "Poop".localized 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/HumidityCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HumidityCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | import SwiftUI 8 | 9 | struct HumidityCompactWidget: View { 10 | let humidity: Int 11 | let dewPoint: String? 12 | var body: some View { 13 | VStack(alignment: .leading) { 14 | HStack(spacing: 5.0) { 15 | Image(systemName: "humidity") 16 | .foregroundColor(.accentColor) 17 | .font(.callout) 18 | Text("Humidity") 19 | .textCase(.uppercase) 20 | .font(.caption) 21 | } 22 | Text("\(humidity)%") 23 | .font(.largeTitle) 24 | .padding(.bottom, 5) 25 | if let dewPoint { 26 | Text("The dew point is \(dewPoint) right now.") 27 | .lineLimit(3) 28 | .allowsTightening(true) 29 | .fixedSize(horizontal: false, vertical: true) 30 | .font(.caption2) 31 | } 32 | } 33 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 34 | .padding() 35 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 36 | } 37 | } 38 | 39 | #Preview { 40 | let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) 41 | Form { 42 | LazyVGrid(columns: gridItemLayout) { 43 | HumidityCompactWidget(humidity: 27, dewPoint: "32°") 44 | HumidityCompactWidget(humidity: 27, dewPoint: nil) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/MeshtasticLogo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshtasticLogo.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 10/6/22. 6 | // 7 | import SwiftUI 8 | 9 | struct MeshtasticLogo: View { 10 | 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | var body: some View { 14 | #if targetEnvironment(macCatalyst) 15 | VStack { 16 | Link(destination: URL(string: "meshtastic:///settings/about")!) { 17 | if #available(iOS 26.0, macOS 26.0, *) { 18 | Image(colorScheme == .dark ? "logo-white" : "logo-black") 19 | .resizable() 20 | .foregroundColor(.accentColor) 21 | .scaledToFit() 22 | } else { 23 | Image("logo-white") 24 | .resizable() 25 | .foregroundColor(.accentColor) 26 | .scaledToFit() 27 | } 28 | } 29 | } 30 | .padding(.bottom, 5) 31 | .padding(.top, 5) 32 | #else 33 | if #available(iOS 26.0, macOS 26.0, *) { 34 | VStack { 35 | Link(destination: URL(string: "meshtastic:///settings/about")!) { 36 | Image(colorScheme == .dark ? "logo-white" : "logo-black") 37 | .resizable() 38 | .scaledToFit() 39 | } 40 | } 41 | } else { 42 | VStack { 43 | Link(destination: URL(string: "meshtastic:///settings/about")!) { 44 | Image(colorScheme == .dark ? "logo-white" : "logo-black") 45 | .resizable() 46 | .scaledToFit() 47 | } 48 | } 49 | .padding(.bottom, 5) 50 | } 51 | #endif 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Meshtastic/Views/Messages/TapbackResponses.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import OSLog 3 | 4 | struct TapbackResponses: View { 5 | @Environment(\.managedObjectContext) var context 6 | 7 | let message: MessageEntity 8 | let onRead: () -> Void 9 | 10 | @ViewBuilder 11 | var body: some View { 12 | let tapbacks = message.tapbacks 13 | if !tapbacks.isEmpty { 14 | VStack(alignment: .trailing) { 15 | HStack { 16 | ForEach( tapbacks ) { (tapback: MessageEntity) in 17 | VStack { 18 | let image = tapback.messagePayload!.image(fontSize: 20) 19 | Image(uiImage: image!).font(.caption) 20 | Text("\(tapback.fromUser?.shortName ?? "?")") 21 | .font(.caption2) 22 | .foregroundColor(.gray) 23 | .fixedSize() 24 | .padding(.bottom, 1) 25 | } 26 | .onAppear { 27 | guard !tapback.read else { 28 | return 29 | } 30 | 31 | tapback.read = true 32 | do { 33 | try context.save() 34 | Logger.data.info("📖 Read tapback \(tapback.messageId, privacy: .public) ") 35 | onRead() 36 | } catch { 37 | Logger.data.error("Failed to read tapback \(tapback.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") 38 | } 39 | } 40 | } 41 | } 42 | .padding(10) 43 | .overlay( 44 | RoundedRectangle(cornerRadius: 18) 45 | .stroke(Color.gray, lineWidth: 1) 46 | ) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/ShutDownNodeIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShutDownNodeIntent.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 8/24/24. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | struct ShutDownNodeIntent: AppIntent { 12 | static let title: LocalizedStringResource = "Shut Down" 13 | 14 | static let description: IntentDescription = "Send a shutdown to the node you are connected to" 15 | 16 | func perform() async throws -> some IntentResult { 17 | try await requestConfirmation(result: .result(dialog: "Shut Down Node?")) 18 | 19 | if !(await AccessoryManager.shared.isConnected) { 20 | throw AppIntentErrors.AppIntentError.notConnected 21 | } 22 | 23 | // Safely unwrap the connectedNode using if let 24 | if let connectedPeripheralNum = await AccessoryManager.shared.activeDeviceNum, 25 | let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), 26 | let fromUser = connectedNode.user, 27 | let toUser = connectedNode.user { 28 | 29 | // Attempt to send shutdown, throw an error if it fails 30 | do { 31 | try await AccessoryManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser) 32 | } catch { 33 | throw AppIntentErrors.AppIntentError.message("Failed to shut down") 34 | } 35 | } else { 36 | throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") 37 | } 38 | return .result() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Meshtastic/Views/Settings/AppIconButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIconButton.swift 3 | // Meshtastic 4 | // 5 | // Created by Chase Christiansen on 7/21/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppIconButton: View { 11 | @Binding var iconDescription: String 12 | @Binding var iconName: String? 13 | @Binding var isPresenting: Bool 14 | @State var errorDetails: String? 15 | @State var didError = false 16 | 17 | @Environment(\.colorScheme) var colorScheme 18 | 19 | var body: some View { 20 | Button { 21 | UIApplication.shared.setAlternateIconName(iconName) { error in 22 | if let error = error { 23 | errorDetails = error.localizedDescription 24 | didError = true 25 | } else { 26 | self.isPresenting = false 27 | } 28 | } 29 | } label: { 30 | HStack(alignment: .center) { 31 | let imageName = colorScheme == .dark ? "\(iconName ?? "AppIcon")_Dark_Thumb" : "\(iconName ?? "AppIcon")_Thumb" 32 | 33 | if let image = UIImage(named: imageName) { 34 | Image(uiImage: image) 35 | .resizable() 36 | .aspectRatio(contentMode: .fill) 37 | .background(.thickMaterial) 38 | .frame(width: 50, height: 50) 39 | .clipShape(RoundedRectangle(cornerRadius: 8)) 40 | } 41 | 42 | VStack(alignment: .leading) { 43 | Text(iconDescription) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | #Preview { 51 | List { 52 | AppIconButton(iconDescription: .constant("Default"), iconName: .constant("AppIcon"), isPresenting: .constant(true)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Meshtastic/Views/Helpers/Compact Widgets/WindCompactWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindCompactWidget.swift 3 | // Meshtastic 4 | // 5 | // Created by Jake Bordens on 3/14/25. 6 | // 7 | import SwiftUI 8 | 9 | struct WindCompactWidget: View { 10 | let speed: String 11 | let gust: String? 12 | let direction: String? 13 | 14 | var body: some View { 15 | let hasGust = ((gust ?? "").isEmpty == false) 16 | VStack(alignment: .leading) { 17 | Label { Text("Wind").textCase(.uppercase) } icon: { Image(systemName: "wind").foregroundColor(.accentColor) } 18 | if let direction { 19 | Text("\(direction)") 20 | .font(!hasGust ? .callout : .caption) 21 | .padding(.bottom, 10) 22 | } 23 | Text(speed) 24 | .font(.system(size: 35)) 25 | if let gust, !gust.isEmpty { 26 | Text("Gusts \(gust)") 27 | } 28 | } 29 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) 30 | .padding() 31 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) 32 | } 33 | } 34 | 35 | #Preview { 36 | let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) 37 | Form { 38 | LazyVGrid(columns: gridItemLayout) { 39 | WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: "SW") 40 | WindCompactWidget(speed: "12 mph", gust: nil, direction: "SW") 41 | WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: nil) 42 | WindCompactWidget(speed: "12 mph", gust: nil, direction: nil) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import MeshtasticProtobufs 3 | 4 | extension MQTTConfigEntity { 5 | convenience init( 6 | context: NSManagedObjectContext, 7 | config: ModuleConfig.MQTTConfig 8 | ) { 9 | self.init(context: context) 10 | self.enabled = config.enabled 11 | self.proxyToClientEnabled = config.proxyToClientEnabled 12 | self.address = config.address 13 | self.username = config.username 14 | self.password = config.password 15 | self.root = config.root 16 | self.encryptionEnabled = config.encryptionEnabled 17 | self.jsonEnabled = config.jsonEnabled 18 | self.tlsEnabled = config.tlsEnabled 19 | self.mapReportingEnabled = config.mapReportingEnabled 20 | self.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) 21 | self.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) 22 | } 23 | 24 | func update(with config: ModuleConfig.MQTTConfig) { 25 | enabled = config.enabled 26 | proxyToClientEnabled = config.proxyToClientEnabled 27 | address = config.address 28 | username = config.username 29 | password = config.password 30 | root = config.root 31 | encryptionEnabled = config.encryptionEnabled 32 | jsonEnabled = config.jsonEnabled 33 | tlsEnabled = config.tlsEnabled 34 | mapReportingEnabled = config.mapReportingEnabled 35 | mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) 36 | mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic/Router/NavigationState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: Messages 4 | 5 | enum MessagesNavigationState: Hashable { 6 | case channels( 7 | channelId: Int32? = nil, 8 | messageId: Int64? = nil 9 | ) 10 | case directMessages( 11 | userNum: Int64? = nil, 12 | messageId: Int64? = nil 13 | ) 14 | } 15 | 16 | // MARK: Map 17 | 18 | enum MapNavigationState: Hashable { 19 | case selectedNode(Int64) 20 | case waypoint(Int64) 21 | } 22 | 23 | // MARK: Settings 24 | 25 | enum SettingsNavigationState: String { 26 | case about 27 | case appSettings 28 | case routes 29 | case routeRecorder 30 | case lora 31 | case channels 32 | case shareQRCode 33 | case user 34 | case bluetooth 35 | case device 36 | case display 37 | case network 38 | case position 39 | case power 40 | case ambientLighting 41 | case cannedMessages 42 | case detectionSensor 43 | case externalNotification 44 | case mqtt 45 | case rangeTest 46 | case paxCounter 47 | case ringtone 48 | case serial 49 | case security 50 | case storeAndForward 51 | case telemetry 52 | case debugLogs 53 | case appFiles 54 | case firmwareUpdates 55 | } 56 | 57 | struct NavigationState: Hashable { 58 | enum Tab: String, Hashable { 59 | case messages 60 | case connect 61 | case nodes 62 | case map 63 | case settings 64 | } 65 | 66 | var selectedTab: Tab = .connect 67 | var messages: MessagesNavigationState? 68 | var nodeListSelectedNodeNum: Int64? 69 | var map: MapNavigationState? 70 | var settings: SettingsNavigationState? 71 | } 72 | -------------------------------------------------------------------------------- /.swiftlint-precommit.yml: -------------------------------------------------------------------------------- 1 | # Exclude automatically generated Swift files 2 | excluded: 3 | - MeshtasticProtobufs 4 | 5 | line_length: 400 6 | 7 | type_name: 8 | min_length: 1 9 | max_length: 10 | warning: 60 11 | error: 70 12 | excluded: iPhone # excluded via string 13 | allowed_symbols: ["_"] # these are allowed in type names 14 | identifier_name: 15 | min_length: 1 16 | max_length: 17 | warning: 60 18 | allowed_symbols: ["_"] # these are allowed in type names 19 | 20 | # TODO: should review 21 | force_try: 22 | severity: warning # explicitly 23 | 24 | # TODO: should review 25 | file_length: 26 | warning: 3500 27 | error: 4000 28 | 29 | # TODO: should review 30 | cyclomatic_complexity: 31 | warning: 70 32 | error: 80 33 | ignores_case_statements: true 34 | 35 | # TODO: should review 36 | function_body_length: 37 | warning: 200 38 | 39 | # TODO: should review 40 | type_body_length: 41 | warning: 400 42 | 43 | # TODO: should review 44 | disabled_rules: # rule identifiers to exclude from running 45 | - operator_whitespace 46 | - multiple_closures_with_trailing_closure 47 | - todo 48 | 49 | # TODO: should review 50 | nesting: 51 | type_level: 52 | warning: 3 53 | 54 | custom_rules: 55 | disable_print: 56 | included: ".*\\.swift" 57 | name: "Disable `print()`" 58 | regex: "((\\bprint)|(Swift\\.print))\\s*\\(" 59 | message: "Consider using a dedicated log message or the Xcode debugger instead of using `print`. ex. logger.debug(...)" 60 | severity: warning 61 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/MessageNodeIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageNodeIntent.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 11/9/24. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | struct MessageNodeIntent: AppIntent { 12 | static var title: LocalizedStringResource = "Send a Direct Message" 13 | 14 | static var description: IntentDescription = "Send a message to a certain meshtastic node" 15 | 16 | @Parameter(title: "Message") 17 | var messageContent: String 18 | 19 | @Parameter(title: "Node Number") 20 | var nodeNumber: Int 21 | 22 | static var parameterSummary: some ParameterSummary { 23 | Summary("Send \(\.$messageContent) to \(\.$nodeNumber)") 24 | } 25 | func perform() async throws -> some IntentResult { 26 | if await !AccessoryManager.shared.isConnected { 27 | throw AppIntentErrors.AppIntentError.notConnected 28 | } 29 | 30 | // Convert messageContent to data and check its length 31 | guard let messageData = messageContent.data(using: .utf8) else { 32 | throw AppIntentErrors.AppIntentError.message("Failed to encode message content") 33 | } 34 | 35 | if messageData.count > 200 { 36 | throw $messageContent.needsValueError("Message content exceeds 200 bytes.") 37 | } 38 | 39 | do { 40 | try await AccessoryManager.shared.sendMessage(message: messageContent, toUserNum: Int64(nodeNumber), channel: 0, isEmoji: false, replyID: 0) 41 | } catch { 42 | throw AppIntentErrors.AppIntentError.message("Failed to send message") 43 | } 44 | 45 | return .result() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Exclude automatically generated Swift files 2 | excluded: 3 | - MeshtasticProtobufs 4 | 5 | line_length: 400 6 | 7 | type_name: 8 | min_length: 1 9 | max_length: 10 | warning: 60 11 | error: 70 12 | excluded: iPhone # excluded via string 13 | allowed_symbols: ["_"] # these are allowed in type names 14 | identifier_name: 15 | min_length: 1 16 | max_length: 17 | warning: 60 18 | allowed_symbols: ["_"] # these are allowed in type names 19 | 20 | # TODO: should review 21 | force_try: 22 | severity: warning # explicitly 23 | 24 | # TODO: should review 25 | file_length: 26 | warning: 3500 27 | error: 4000 28 | 29 | # TODO: should review 30 | cyclomatic_complexity: 31 | warning: 60 32 | error: 70 33 | ignores_case_statements: true 34 | 35 | # TODO: should review 36 | function_body_length: 37 | warning: 200 38 | 39 | # TODO: should review 40 | type_body_length: 41 | warning: 400 42 | 43 | # TODO: should review 44 | disabled_rules: # rule identifiers to exclude from running 45 | - operator_whitespace 46 | - multiple_closures_with_trailing_closure 47 | - todo 48 | - trailing_whitespace 49 | 50 | # TODO: should review 51 | nesting: 52 | type_level: 53 | warning: 3 54 | 55 | custom_rules: 56 | disable_print: 57 | included: ".*\\.swift" 58 | name: "Disable `print()`" 59 | regex: "((\\bprint)|(Swift\\.print))\\s*\\(" 60 | message: "Consider using a dedicated log message or the Xcode debugger instead of using `print`. ex. logger.debug(...)" 61 | severity: warning 62 | -------------------------------------------------------------------------------- /Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import OSLog 3 | import SwiftUI 4 | 5 | struct IgnoreNodeButton: View { 6 | @Environment(\.managedObjectContext) var context 7 | @EnvironmentObject var accessoryManager: AccessoryManager 8 | 9 | @ObservedObject 10 | var node: NodeInfoEntity 11 | 12 | var body: some View { 13 | Button(role: .destructive) { 14 | guard let connectedNodeNum = accessoryManager.activeDeviceNum else { return } 15 | Task { 16 | do { 17 | if node.ignored { 18 | try await accessoryManager.removeIgnoredNode( 19 | node: node, 20 | connectedNodeNum: Int64(connectedNodeNum) 21 | ) 22 | } else { 23 | try await accessoryManager.setIgnoredNode( 24 | node: node, 25 | connectedNodeNum: Int64(connectedNodeNum) 26 | ) 27 | } 28 | Task {@MainActor in 29 | // CoreData Stuff 30 | node.ignored = !node.ignored 31 | do { 32 | try context.save() 33 | } catch { 34 | context.rollback() 35 | Logger.data.error("Save Ignored Node Error") 36 | } 37 | } 38 | Logger.data.debug("Ignored a node") 39 | } catch { 40 | Logger.mesh.error("Faile to Ignored/Un-ignore a node") 41 | } 42 | } 43 | } label: { 44 | Label { 45 | Text(node.ignored ? "Remove from ignored" : "Ignore Node") 46 | } icon: { 47 | Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") 48 | .symbolRenderingMode(.multicolor) 49 | } 50 | // Accessibility: Label for VoiceOver 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Meshtastic/Enums/DetectionSensorEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetectionSensorEnums.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 10/11/24. 6 | // 7 | import MeshtasticProtobufs 8 | 9 | enum TriggerTypes: Int, CaseIterable, Identifiable { 10 | 11 | case logicLow = 0 12 | case logicHigh = 1 13 | case fallingEdge = 2 14 | case risingEdge = 3 15 | case eitherEdgeActiveLow = 4 16 | case eitherEdgeActiveHigh = 5 17 | 18 | var id: Int { self.rawValue } 19 | 20 | var name: String { 21 | switch self { 22 | case .logicLow: 23 | return "Low" 24 | case .logicHigh: 25 | return "High" 26 | case .fallingEdge: 27 | return "Falling Edge" 28 | case .risingEdge: 29 | return "Rising Edge" 30 | case .eitherEdgeActiveLow: 31 | return "Either Edge Low" 32 | case .eitherEdgeActiveHigh: 33 | return "Either Edge Hight" 34 | } 35 | } 36 | func protoEnumValue() -> ModuleConfig.DetectionSensorConfig.TriggerType { 37 | 38 | switch self { 39 | case .logicLow: 40 | return ModuleConfig.DetectionSensorConfig.TriggerType.logicLow 41 | case .logicHigh: 42 | return ModuleConfig.DetectionSensorConfig.TriggerType.logicHigh 43 | case .fallingEdge: 44 | return ModuleConfig.DetectionSensorConfig.TriggerType.fallingEdge 45 | case .risingEdge: 46 | return ModuleConfig.DetectionSensorConfig.TriggerType.risingEdge 47 | case .eitherEdgeActiveLow: 48 | return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveLow 49 | case .eitherEdgeActiveHigh: 50 | return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveHigh 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Meshtastic/Helpers/EmojiOnlyTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiKeyboard.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 1/10/23. 6 | // 7 | import SwiftUI 8 | 9 | class SwiftUIEmojiTextField: UITextField { 10 | 11 | func setEmoji() { 12 | _ = self.textInputMode 13 | } 14 | 15 | override var textInputContextIdentifier: String? { 16 | return "" 17 | } 18 | 19 | override var textInputMode: UITextInputMode? { 20 | for mode in UITextInputMode.activeInputModes where mode.primaryLanguage == "emoji" { 21 | self.keyboardType = .default // do not remove this 22 | return mode 23 | } 24 | return nil 25 | } 26 | } 27 | 28 | struct EmojiOnlyTextField: UIViewRepresentable { 29 | @Binding var text: String 30 | var placeholder: String = "" 31 | 32 | func makeUIView(context: Context) -> SwiftUIEmojiTextField { 33 | let emojiTextField = SwiftUIEmojiTextField() 34 | emojiTextField.placeholder = placeholder 35 | emojiTextField.text = text 36 | emojiTextField.delegate = context.coordinator 37 | return emojiTextField 38 | } 39 | 40 | func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) { 41 | uiView.text = text 42 | } 43 | 44 | func makeCoordinator() -> Coordinator { 45 | Coordinator(parent: self) 46 | } 47 | 48 | class Coordinator: NSObject, UITextFieldDelegate { 49 | var parent: EmojiOnlyTextField 50 | init(parent: EmojiOnlyTextField) { 51 | self.parent = parent 52 | } 53 | func textFieldDidChangeSelection(_ textField: UITextField) { 54 | DispatchQueue.main.async { [weak self] in 55 | self?.parent.text = textField.text ?? "" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Meshtastic/Accessory/Helpers/ManualConnectionList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManualConnectionList.swift 3 | // Meshtastic 4 | // 5 | // Created by jake on 10/26/25. 6 | // 7 | 8 | import Foundation 9 | 10 | // Maintains an observable list of devices that's backed by UserDefaults 11 | public class ManualConnectionList: ObservableObject { 12 | static let shared = ManualConnectionList() 13 | 14 | @Published private var _list: [Device] 15 | 16 | private init() { 17 | _list = UserDefaults.manualConnections 18 | } 19 | 20 | var connectionsList: [Device] { 21 | get { 22 | return _list 23 | } 24 | } 25 | 26 | func insert(device: Device) { 27 | // Don't insert if already there 28 | guard !_list.contains(where: {$0.id == device.id}) else { 29 | return 30 | } 31 | 32 | // Add the new entry 33 | var list = _list 34 | list.append(device) 35 | _list = list 36 | UserDefaults.manualConnections = list 37 | } 38 | 39 | func updateDevice(deviceId: UUID, key: WritableKeyPath, value: T) where T: Equatable { 40 | var list = _list 41 | if let deviceIndex = list.firstIndex(where: {$0.id == deviceId}) { 42 | list[deviceIndex][keyPath: key] = value 43 | _list = list 44 | UserDefaults.manualConnections = list 45 | } 46 | } 47 | 48 | func remove(device: Device) { 49 | var list = _list 50 | list.removeAll(where: {$0.id == device.id}) 51 | _list = list 52 | UserDefaults.manualConnections = list 53 | } 54 | 55 | func remove(atOffsets: IndexSet) { 56 | var list = _list 57 | list.remove(atOffsets: atOffsets) 58 | _list = list 59 | UserDefaults.manualConnections = list 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Meshtastic/Views/Settings/Config/ConfigHeader.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CoreData 3 | 4 | struct ConfigHeader: View { 5 | @EnvironmentObject var accessoryManager: AccessoryManager 6 | 7 | let title: String 8 | let config: KeyPath 9 | let node: NodeInfoEntity? 10 | let onAppear: () -> Void 11 | 12 | var body: some View { 13 | if node != nil && node?.metadata == nil && node?.num ?? 0 != accessoryManager.activeDeviceNum ?? 0 { 14 | Text("There has been no response to a request for device metadata via PKC admin for this node.") 15 | .font(.callout) 16 | .foregroundColor(.orange) 17 | 18 | } else if node != nil && node?.num ?? 0 != accessoryManager.activeDeviceNum ?? 0 { 19 | // Let users know what is going on if they are using remote admin and don't have the config yet 20 | let expiration = node?.sessionExpiration ?? Date() 21 | if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() { 22 | Text("\(title) config data was requested via PKC admin but no response has been returned from the remote node.") 23 | .font(.callout) 24 | .foregroundColor(.orange) 25 | } else { 26 | Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") 27 | .onFirstAppear(onAppear) 28 | .font(.title3) 29 | } 30 | } else if node != nil && node?.num ?? 0 == accessoryManager.activeDeviceNum ?? -1 { 31 | Text("Configuration for: \(node?.user?.longName ?? "Unknown")") 32 | .onFirstAppear(onAppear) 33 | } else { 34 | Text("Please connect to a radio to configure settings.") 35 | .font(.callout) 36 | .foregroundColor(.orange) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/ShortcutsProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutsProvider.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 8/24/24. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | struct ShortcutsProvider: AppShortcutsProvider { 12 | static var appShortcuts: [AppShortcut] { 13 | AppShortcut(intent: ShutDownNodeIntent(), 14 | phrases: ["Shut down \(.applicationName) node", 15 | "Shut down my \(.applicationName) node", 16 | "Turn off \(.applicationName) node", 17 | "Power down \(.applicationName) node", 18 | "Deactivate \(.applicationName) node"], 19 | shortTitle: "Shut Down", 20 | systemImageName: "power") 21 | 22 | AppShortcut(intent: RestartNodeIntent(), 23 | phrases: ["Restart \(.applicationName) node", 24 | "Restart my \(.applicationName) node", 25 | "Reboot \(.applicationName) node", 26 | "Reboot my \(.applicationName) node"], 27 | shortTitle: "Restart", 28 | systemImageName: "arrow.circlepath") 29 | 30 | AppShortcut(intent: MessageChannelIntent(), 31 | phrases: ["Message a \(.applicationName) channel", 32 | "Send a \(.applicationName) group message"], 33 | shortTitle: "Group Message", 34 | systemImageName: "message") 35 | AppShortcut(intent: DisconnectNodeIntent(), 36 | phrases: ["Disconnect \(.applicationName) node", 37 | "Disconnect my \(.applicationName) node", 38 | "Disconnect from \(.applicationName)", 39 | "Disconnect \(.applicationName)"], 40 | shortTitle: "Disconnect", 41 | systemImageName: "antenna.radiowaves.left.and.right.slash") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Meshtastic/AppIntents/AddContactIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddContactIntent.swift 3 | // Meshtastic 4 | // 5 | // Created by Benjamin Faershtein on 5/13/25. 6 | // 7 | 8 | import AppIntents 9 | import MeshtasticProtobufs 10 | 11 | struct AddContactIntent: AppIntent { 12 | static let title: LocalizedStringResource = "Add Contact" 13 | static let description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" 14 | 15 | @Parameter(title: "Contact URL", description: "The URL for the node to add") 16 | var contactUrl: URL 17 | 18 | // Define the function that performs the main logic 19 | func perform() async throws -> some IntentResult { 20 | // Ensure the BLE Manager is connected 21 | if !(await AccessoryManager.shared.isConnected) { 22 | throw AppIntentErrors.AppIntentError.notConnected 23 | } 24 | 25 | if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") { 26 | let components = self.contactUrl.absoluteString.components(separatedBy: "#") 27 | // Extract contact information from the URL 28 | if let contactData = components.last { 29 | let decodedString = contactData.base64urlToBase64() 30 | if let _ = Data(base64Encoded: decodedString) { 31 | do { 32 | try await AccessoryManager.shared.addContactFromURL(base64UrlString: contactData) 33 | } catch { 34 | throw AppIntentErrors.AppIntentError.message("Failed to add/parse contact data: \(error.localizedDescription)") 35 | } 36 | } 37 | } 38 | // Return a success result 39 | return .result() 40 | } else { 41 | throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link") 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Meshtastic/Tips/ChannelTips.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelTips.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 8/31/23. 6 | // 7 | import SwiftUI 8 | import TipKit 9 | 10 | struct ShareChannelsTip: Tip { 11 | 12 | var id: String { 13 | return "tip.channels.share" 14 | } 15 | var title: Text { 16 | Text("Sharing Meshtastic Channels") 17 | } 18 | var message: Text? { 19 | Text("A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio.") 20 | } 21 | var image: Image? { 22 | Image(systemName: "qrcode") 23 | } 24 | } 25 | 26 | struct CreateChannelsTip: Tip { 27 | 28 | var id: String { 29 | return "tip.channels.create" 30 | } 31 | var title: Text { 32 | Text("Manage Channels") 33 | } 34 | var message: Text? { 35 | Text("Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)") 36 | } 37 | var image: Image? { 38 | Image(systemName: "fibrechannel") 39 | } 40 | } 41 | 42 | struct AdminChannelTip: Tip { 43 | 44 | var id: String { 45 | return "tip.channel.admin" 46 | } 47 | var title: Text { 48 | Text("Administration Enabled") 49 | } 50 | var message: Text? { 51 | Text("Select a node from the drop down to manage connected or remote devices.") 52 | } 53 | var image: Image? { 54 | Image(systemName: "fibrechannel") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 Feature Request" 2 | description: Request a new feature 3 | title: "🚀 [Feature Request]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for your request this will not gurantee that we will implement it, but it will be reviewed. 10 | - type: dropdown 11 | id: soc 12 | attributes: 13 | label: OS 14 | description: What OS will support your feature? 15 | multiple: true 16 | options: 17 | - iOS 18 | - iPadOS 19 | - macOS 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: body 24 | attributes: 25 | label: Description 26 | description: Please provide details about your enhancement. 27 | validations: 28 | required: true 29 | - type: checkboxes 30 | attributes: 31 | label: Participation 32 | description: (Features without participation go to the backlog.) 33 | options: 34 | - label: I am willing to pay to sponsor this feature. 35 | required: false 36 | - label: I am willing to submit a pull request for this feature. 37 | required: false 38 | - type: textarea 39 | attributes: 40 | label: Additional comments 41 | description: Is there anything else that's important for the team to know? 42 | - type: checkboxes 43 | id: terms 44 | attributes: 45 | label: Code of Conduct 46 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://meshtastic.org/docs/legal/conduct/). 47 | options: 48 | - label: I agree to follow this project's Code of Conduct 49 | required: true 50 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyInfoEntityExtension.swift 3 | // Meshtastic 4 | // 5 | // Copyright(c) Garth Vander Houwen 9/3/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension MyInfoEntity { 12 | var messagePredicate: NSPredicate { 13 | return NSPredicate(format: "toUser == nil AND isEmoji == false") 14 | } 15 | 16 | var messageFetchRequest: NSFetchRequest { 17 | let fetchRequest = MessageEntity.fetchRequest() 18 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] 19 | fetchRequest.predicate = messagePredicate 20 | return fetchRequest 21 | } 22 | 23 | var messageList: [MessageEntity] { 24 | let context = PersistenceController.shared.container.viewContext 25 | let fetchRequest = messageFetchRequest 26 | 27 | return (try? context.fetch(messageFetchRequest)) ?? [MessageEntity]() 28 | } 29 | 30 | func unreadMessages(context: NSManagedObjectContext) -> Int { 31 | // Returns the count of unread *channel* messages 32 | let fetchRequest = messageFetchRequest 33 | fetchRequest.sortDescriptors = [] // sort is irrelevant. 34 | fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) 35 | 36 | return (try? context.count(for: fetchRequest)) ?? 0 37 | } 38 | 39 | // Backwards-compatible property (uses viewContext) 40 | var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } 41 | 42 | var hasAdmin: Bool { 43 | let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } 44 | return adminChannel?.count ?? 0 > 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Meshtastic 2 | 3 | This document outlines the process for preparing and making a release for Meshtastic. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Branching Strategy](#branching-strategy) 8 | 2. [Preparing for a Release](#preparing-for-a-release) 9 | 3. [Creating a Release Branch](#creating-a-release-branch) 10 | 4. [Finalizing the Release](#finalizing-the-release) 11 | 12 | ## Branching Strategy 13 | 14 | - **Main Branch (`main`)**: This is the main development branch where daily development occurs. 15 | - **Release Branch (`X.YY.ZZ-release`)**: This branch is created from `main` for preparing a specific release version. 16 | 17 | ## Preparing for a Release 18 | 19 | 1. Ensure all desired features and fixes are merged into the `main` branch. 20 | 2. Update the version number in the relevant files. 21 | 3. Update the project documentation to reflect the upcoming release. 22 | 23 | ## Creating a Release Branch 24 | 25 | 1. Create a release branch from `main`. 26 | ```sh 27 | ./scripts/create-release-branch.sh 28 | ``` 29 | 30 | ## Finalizing the Release 31 | 32 | 1. Perform final testing and quality checks on the `X.YY.ZZ-release` branch. 33 | a. If any hotfix changes are required, merge those changes into `X.YY.ZZ-release`. 34 | b. After merging these changes into the release branch, cherry-pick the changes onto `main`. 35 | 2. Once everything is ready, create a final tag for the release: 36 | ```sh 37 | git tag -a X.YY.ZZ -m "Release version X.Y.Z" 38 | git push origin X.YY.ZZ 39 | ``` 40 | 41 | Thank you for following the release process and helping to ensure the stability and quality of Meshtastic! 42 | 43 | --- 44 | 45 | Feel free to modify this template to better fit your project's specific needs. --------------------------------------------------------------------------------