├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── feature.yml
├── pull_request_template.md
└── workflows
│ ├── stale.yml
│ └── swiftlint.yml
├── .gitignore
├── .gitmodules
├── .swiftlint.yml
├── CONTRIBUTING.md
├── LICENSE
├── Localizable.xcstrings
├── Meshtastic.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── xcshareddata
│ └── xcschemes
│ ├── Meshtastic.xcscheme
│ └── WidgetsExtension.xcscheme
├── Meshtastic.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ ├── WorkspaceSettings.xcsettings
│ └── swiftpm
│ └── Package.resolved
├── Meshtastic
├── AppIntents
│ ├── AddContactIntent.swift
│ ├── AppIntentErrors.swift
│ ├── DisconnectNodeIntent.swift
│ ├── FactoryResetNodeIntent.swift
│ ├── MessageChannelIntent.swift
│ ├── MessageNodeIntent.swift
│ ├── NavigateToNodeIntent.swift
│ ├── NodePositionIntent.swift
│ ├── RestartNodeIntent.swift
│ ├── SaveChannelSettingsIntent.swift
│ ├── SendWaypointIntent.swift
│ ├── ShortcutsProvider.swift
│ └── ShutDownNodeIntent.swift
├── AppState.swift
├── Assets.xcassets
│ ├── ANDROIDSIM.imageset
│ │ ├── Contents.json
│ │ └── play_store_icon_114px-4.png
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── logo-3.png
│ │ ├── logo-dark.png
│ │ └── logo-tinted.png
│ ├── Color.colorset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── HELTECHT62.imageset
│ │ ├── Contents.json
│ │ └── heltec-ht62-esp32c3-sx1262.svg
│ ├── HELTECMESHNODET114.imageset
│ │ ├── Contents.json
│ │ └── heltec-mesh-node-t114-case.svg
│ ├── HELTECV3.imageset
│ │ ├── Contents.json
│ │ └── heltec-v3-case.svg
│ ├── HELTECVISIONMASTERE213.imageset
│ │ ├── Contents.json
│ │ └── heltec-vision-master-e213.svg
│ ├── HELTECVISIONMASTERE290.imageset
│ │ ├── Contents.json
│ │ └── heltec-vision-master-e290.svg
│ ├── HELTECWIRELESSPAPER.imageset
│ │ ├── Contents.json
│ │ └── heltec-wireless-paper.svg
│ ├── HELTECWIRELESSTRACKER.imageset
│ │ ├── Contents.json
│ │ └── heltec-wireless-tracker.svg
│ ├── HELTECWSLV3.imageset
│ │ ├── Contents.json
│ │ └── heltec-wsl-v3.svg
│ ├── LILYGOTBEAMS3CORE.imageset
│ │ ├── Contents.json
│ │ └── tbeam-s3-core.svg
│ ├── NANOG1.imageset
│ │ ├── 2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png
│ │ └── Contents.json
│ ├── NANOG2ULTRA.imageset
│ │ ├── Contents.json
│ │ └── nano-g2-ultra.svg
│ ├── PROMICRO.imageset
│ │ ├── Contents.json
│ │ └── promicro.svg
│ ├── RAK11310.imageset
│ │ ├── Contents.json
│ │ └── rak11310.svg
│ ├── RAK4631.imageset
│ │ ├── Contents.json
│ │ └── rak4631_case.svg
│ ├── RPIPICO.imageset
│ │ ├── Contents.json
│ │ └── pico.svg
│ ├── SEEEDXIAOS3.imageset
│ │ ├── Contents.json
│ │ └── seeed-xiao-s3.svg
│ ├── SENSECAPINDICATOR.imageset
│ │ ├── Contents.json
│ │ └── seeed-sensecap-indicator.svg
│ ├── SOLAR_NODE.imageset
│ │ ├── Contents.json
│ │ └── solar_node.png
│ ├── STATIONG1.imageset
│ │ ├── Contents.json
│ │ └── meshtastic_mesh_device_station_edition_overview 1.png
│ ├── STATIONG2.imageset
│ │ ├── Contents.json
│ │ └── station-g2.svg
│ ├── TBEAM.imageset
│ │ ├── Contents.json
│ │ └── tbeam.svg
│ ├── TDECK.imageset
│ │ ├── Contents.json
│ │ └── t-deck.svg
│ ├── TECHO.imageset
│ │ ├── Contents.json
│ │ └── t-echo.svg
│ ├── THINKNODEM1.imageset
│ │ ├── Contents.json
│ │ └── thinknode_m1.svg
│ ├── THINKNODEM2.imageset
│ │ ├── Contents.json
│ │ └── thinknode_m2.svg
│ ├── TLORAC6.imageset
│ │ ├── Contents.json
│ │ └── tlora-c6.svg
│ ├── TLORAT3S3EPAPER.imageset
│ │ ├── Contents.json
│ │ └── tlora-t3s3-epaper.svg
│ ├── TLORAT3S3V1.imageset
│ │ ├── Contents.json
│ │ └── tlora-t3s3-v1.svg
│ ├── TLORAV211P6.imageset
│ │ ├── Contents.json
│ │ └── tlora-v2-1-1_6.svg
│ ├── TLORAV211P8.imageset
│ │ ├── Contents.json
│ │ └── tlora-v2-1-1_8.svg
│ ├── TRACKERT1000E.imageset
│ │ ├── Contents.json
│ │ └── tracker-t1000-e.svg
│ ├── TWATCHS3.imageset
│ │ ├── Contents.json
│ │ └── t-watch-s3.svg
│ ├── UNPHONE.imageset
│ │ ├── Contents.json
│ │ └── UNPHONE.png
│ ├── UNSET.imageset
│ │ ├── Contents.json
│ │ └── unknown.svg
│ ├── WIOWM1110.imageset
│ │ ├── Contents.json
│ │ └── wio-tracker-wm1110.svg
│ ├── WISMESHTAP.imageset
│ │ ├── Contents.json
│ │ └── rak-wismeshtap.svg
│ ├── XIAONRF52KIT.imageset
│ │ ├── Contents.json
│ │ └── seeed_xiao_nrf52_kit.svg
│ ├── logo-black.imageset
│ │ ├── Contents.json
│ │ └── Mesh_Logo_Black.svg
│ ├── logo-white.imageset
│ │ ├── Contents.json
│ │ └── Mesh_Logo_White.svg
│ ├── progress.ring.dashed.symbolset
│ │ ├── Contents.json
│ │ └── progress.ring.dashed.svg
│ ├── soil.moisture.symbolset
│ │ ├── Contents.json
│ │ └── soilMoisture.variable.svg
│ └── soil.temperature.symbolset
│ │ ├── Contents.json
│ │ └── soilTemp.variable.svg
├── Enums
│ ├── AppSettingsEnums.swift
│ ├── BluetoothModes.swift
│ ├── CannedMessagesConfigEnums.swift
│ ├── ChannelRoles.swift
│ ├── DetectionSensorEnums.swift
│ ├── DeviceEnums.swift
│ ├── DisplayEnums.swift
│ ├── EthernetModes.swift
│ ├── IntervalEnums.swift
│ ├── LoraConfigEnums.swift
│ ├── MessageDestination.swift
│ ├── MessagingEnums.swift
│ ├── PositionConfigEnums.swift
│ ├── RouteEnums.swift
│ ├── RoutingError.swift
│ ├── SerialConfigEnums.swift
│ ├── TelemetryEnums.swift
│ └── TelemetryWeather.swift
├── Export
│ ├── CsvDocument.swift
│ ├── LogDocument.swift
│ └── WriteCsvFile.swift
├── Extensions
│ ├── Bundle.swift
│ ├── CLLocation.swift
│ ├── CLLocationCoordinate2D.swift
│ ├── Character.swift
│ ├── Color.swift
│ ├── Constants.swift
│ ├── CoreData
│ │ ├── ChannelEntityExtension.swift
│ │ ├── DeviceMetadataEntityExtension.swift
│ │ ├── ExternalNotificationConfigEntityExtension.swift
│ │ ├── LocationEntityExtension.swift
│ │ ├── MQTTConfigEntityExtension.swift
│ │ ├── ManagedAttributePropertyWrapper.swift
│ │ ├── MessageEntityExtension.swift
│ │ ├── MyInfoEntityExtension.swift
│ │ ├── NodeInfoEntityExtension.swift
│ │ ├── NodeInfoEntityToNodeInfo.swift
│ │ ├── PositionEntityExtension.swift
│ │ ├── RangeTestConfigEntityExtension.swift
│ │ ├── SerialConfigEntityExtension.swift
│ │ ├── StoreForwardConfigEntityExtension.swift
│ │ ├── TraceRouteEntityExtension.swift
│ │ ├── UserEntityExtension.swift
│ │ └── WaypointEntityExtension.swift
│ ├── Data.swift
│ ├── Date.swift
│ ├── Double.swift
│ ├── FileManager.swift
│ ├── Float.swift
│ ├── Int.swift
│ ├── Logger.swift
│ ├── Measurement.swift
│ ├── OSLogEntryLog.swift
│ ├── Protobufs
│ │ └── NodeInfoExtensions.swift
│ ├── String.swift
│ ├── TimeZone.swift
│ ├── UIColor.swift
│ ├── UIImage.swift
│ ├── Url.swift
│ ├── UserDefaults.swift
│ └── View.swift
├── Helpers
│ ├── BLEManager.swift
│ ├── BluetoothManager.swift
│ ├── CommonRegex.swift
│ ├── EmojiOnlyTextField.swift
│ ├── LocalNotificationManager.swift
│ ├── LocationHelper.swift
│ ├── LocationsHandler.swift
│ ├── Logger.swift
│ ├── MeshPackets.swift
│ ├── Mqtt
│ │ └── MqttClientProxyManager.swift
│ └── Preferences.swift
├── Info.plist
├── Measurement
│ └── CustomFormatters.swift
├── Meshtastic.entitlements
├── Meshtastic.xcdatamodeld
│ ├── .xccurrentversion
│ ├── MeshtasticDataModel.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 23.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 24.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 25.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 26.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 27.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 28.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 29.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 30.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 31.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 34.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 35.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 36.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 37.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 38.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 39.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 40.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 41.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 42.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 43.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 44.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 45.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 46.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 47.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 48.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 49.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 50.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV 51.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV10.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV11.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV12.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV13.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV14.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV15.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV16.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV17.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV18.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV19.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV2.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV20.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV21.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV22.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV3.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV32.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV33.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV4.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV5.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV6.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV7.xcdatamodel
│ │ └── contents
│ ├── MeshtasticDataModelV8.xcdatamodel
│ │ └── contents
│ └── MeshtasticDataModelV9.xcdatamodel
│ │ └── contents
├── MeshtasticApp.swift
├── MeshtasticAppDelegate.swift
├── Model
│ ├── CoreData
│ │ ├── TelemetryEntity+CoreDataClass.swift
│ │ └── TelemetryEntity+CoreDataProperties.swift
│ ├── Metrics Visualization
│ │ ├── MetricTableColumn.swift
│ │ ├── MetricsChartSeries.swift
│ │ ├── MetricsColumnList.swift
│ │ └── MetricsSeriesList.swift
│ └── PeripheralModel.swift
├── Persistence
│ ├── Persistence.swift
│ ├── QueryCoreData.swift
│ └── UpdateCoreData.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── RELEASENOTES.md
├── Resources
│ ├── Assets.xcassets
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ ├── 1024.png
│ │ │ │ ├── 120-1.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
│ │ │ │ └── Contents.json
│ │ │ ├── Color.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── HELTECV20.imageset
│ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png
│ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png
│ │ │ │ ├── 655DCEC0-309D-430A-AF50-2453B6ADB1F6.png
│ │ │ │ └── Contents.json
│ │ │ ├── TLORAV2.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── TLORAV211p6.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── UNSET.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── rak4631.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive-1.png
│ │ │ │ └── RAK7205_Enclosure-With-Solar-Panel_Top-View_01_9ed42002-fb51-4c49-a69e-43fcef692ef6_739x@2x.progressive.png
│ │ │ ├── tbeam.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── tbeam-1.jpg
│ │ │ │ ├── tbeam-2.jpg
│ │ │ │ └── tbeam.jpg
│ │ │ ├── techo.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── techo-1.jpg
│ │ │ │ ├── techo-2.jpg
│ │ │ │ └── techo.jpg
│ │ │ └── tlorav1.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── tlora-1.jpeg
│ │ │ │ ├── tlora-2.jpeg
│ │ │ │ └── tlora.jpeg
│ │ └── TBEAM.imageset
│ │ │ ├── tbeam-1.jpg
│ │ │ └── tbeam.jpg
│ ├── DeviceHardware.json
│ └── alpha.png
├── Router
│ ├── NavigationState.swift
│ └── Router.swift
├── Tips
│ ├── BluetoothTips.swift
│ ├── ChannelTips.swift
│ └── MessagesTips.swift
└── Views
│ ├── Bluetooth
│ ├── Connect.swift
│ └── InvalidVersion.swift
│ ├── ContentView.swift
│ ├── Helpers
│ ├── BLESignalStrengthIndicator.swift
│ ├── BatteryCompact.swift
│ ├── BatteryGauge.swift
│ ├── CircleText.swift
│ ├── Compact Widgets
│ │ ├── CompactWidget.swift
│ │ ├── CurrentConditionsCompact.swift
│ │ ├── DistanceCompactWidget.swift
│ │ ├── HumidityCompactWidget.swift
│ │ ├── PressureCompactWidget.swift
│ │ ├── RadiationCompactWidget.swift
│ │ ├── RainfallCompactWidget.swift
│ │ ├── SoilCompactWidgets.swift
│ │ ├── WeatherConditionsCompactWidget.swift
│ │ ├── WeightCompactWidget.swift
│ │ └── WindCompactWidget.swift
│ ├── ConnectedDevice.swift
│ ├── DateTimeText.swift
│ ├── DistanceText.swift
│ ├── Help
│ │ ├── AckErrors.swift
│ │ ├── DirectMessagesHelp.swift
│ │ └── LockLegend.swift
│ ├── LoRaSignalStrength.swift
│ ├── LoRaSignalStrengthIndicator.swift
│ ├── MQTTIcon.swift
│ ├── MeshtasticLogo.swift
│ ├── Messages
│ │ └── MessageTemplate.swift
│ ├── PowerMetrics.swift
│ ├── RateLimitedButton.swift
│ ├── SecureInput.swift
│ └── Weather
│ │ ├── AirQualityIndex.swift
│ │ ├── IAQScale.swift
│ │ ├── IndoorAirQuality.swift
│ │ ├── LocalWeatherConditions.swift
│ │ └── NodeWeatherForecast.swift
│ ├── Layouts
│ └── TraceRoute.swift
│ ├── Messages
│ ├── ChannelList.swift
│ ├── ChannelMessageList.swift
│ ├── MessageContextMenuItems.swift
│ ├── MessageText.swift
│ ├── Messages.swift
│ ├── RetryButton.swift
│ ├── TapbackResponses.swift
│ ├── TextMessageField
│ │ ├── AlertButton.swift
│ │ ├── RequestPositionButton.swift
│ │ ├── TextMessageField.swift
│ │ └── TextMessageSize.swift
│ ├── UserList.swift
│ └── UserMessageList.swift
│ ├── Nodes
│ ├── DetectionSensorLog.swift
│ ├── DeviceMetricsLog.swift
│ ├── EnvironmentMetricsLog.swift
│ ├── Helpers
│ │ ├── Actions
│ │ │ ├── ClientHistoryButton.swift
│ │ │ ├── DeleteNodeButton.swift
│ │ │ ├── ExchangePositionsButton.swift
│ │ │ ├── FavoriteNodeButton.swift
│ │ │ ├── IgnoreNodeButton.swift
│ │ │ ├── NavigateToButton.swift
│ │ │ ├── NodeAlertsButton.swift
│ │ │ └── TraceRouteButton.swift
│ │ ├── Map
│ │ │ ├── MapContent
│ │ │ │ ├── MeshMapContent.swift
│ │ │ │ └── NodeMapContent.swift
│ │ │ ├── MapSettingsForm.swift
│ │ │ ├── NodeMapSwiftUI.swift
│ │ │ ├── PositionAltitudeChart.swift
│ │ │ ├── PositionPopover.swift
│ │ │ └── WaypointForm.swift
│ │ ├── Metrics Columns
│ │ │ ├── EnvironmentDefaultColumns.swift
│ │ │ ├── EnvironmentDefaultSeries.swift
│ │ │ └── MetricsColumnDetail.swift
│ │ ├── NodeDetail.swift
│ │ ├── NodeInfo.swift
│ │ ├── NodeInfoItem.swift
│ │ ├── NodeListFilter.swift
│ │ ├── NodeListItem.swift
│ │ ├── ScrollToBottomButton.swift
│ │ └── ShareContactQRDialog.swift
│ ├── MeshMap.swift
│ ├── NodeList.swift
│ ├── NodeRow.swift
│ ├── PaxCounterLog.swift
│ ├── PositionLog.swift
│ ├── PowerMetricsLog.swift
│ └── TraceRouteLog.swift
│ └── Settings
│ ├── About.swift
│ ├── AppData.swift
│ ├── AppLog.swift
│ ├── AppSettings.swift
│ ├── Channels.swift
│ ├── Channels
│ └── ChannelForm.swift
│ ├── Config
│ ├── BluetoothConfig.swift
│ ├── ConfigHeader.swift
│ ├── DeviceConfig.swift
│ ├── DisplayConfig.swift
│ ├── LoRaConfig.swift
│ ├── Module
│ │ ├── AmbientLightingConfig.swift
│ │ ├── CannedMessagesConfig.swift
│ │ ├── DetectionSensorConfig.swift
│ │ ├── ExternalNotificationConfig.swift
│ │ ├── MQTTConfig.swift
│ │ ├── PaxCounterConfig.swift
│ │ ├── RangeTestConfig.swift
│ │ ├── RtttlConfig.swift
│ │ ├── SerialConfig.swift
│ │ ├── StoreForwardConfig.swift
│ │ └── TelemetryConfig.swift
│ ├── NetworkConfig.swift
│ ├── PositionConfig.swift
│ ├── PowerConfig.swift
│ ├── SaveConfigButton.swift
│ └── SecurityConfig.swift
│ ├── Firmware.swift
│ ├── FirmwareApi.swift
│ ├── GPSStatus.swift
│ ├── Logs
│ ├── AppLogFilter.swift
│ └── LogDetail.swift
│ ├── RouteRecorder.swift
│ ├── Routes.swift
│ ├── SaveChannelQRCode.swift
│ ├── Settings.swift
│ ├── ShareChannels.swift
│ └── UserConfig.swift
├── MeshtasticProtobufs
├── .gitignore
├── Package.swift
└── Sources
│ └── meshtastic
│ ├── admin.pb.swift
│ ├── apponly.pb.swift
│ ├── atak.pb.swift
│ ├── cannedmessages.pb.swift
│ ├── channel.pb.swift
│ ├── clientonly.pb.swift
│ ├── config.pb.swift
│ ├── connection_status.pb.swift
│ ├── device_ui.pb.swift
│ ├── deviceonly.pb.swift
│ ├── interdevice.pb.swift
│ ├── localonly.pb.swift
│ ├── mesh.pb.swift
│ ├── module_config.pb.swift
│ ├── mqtt.pb.swift
│ ├── paxcount.pb.swift
│ ├── portnums.pb.swift
│ ├── powermon.pb.swift
│ ├── remote_hardware.pb.swift
│ ├── rtttl.pb.swift
│ ├── storeforward.pb.swift
│ ├── telemetry.pb.swift
│ └── xmodem.pb.swift
├── MeshtasticTests
└── RouterTests.swift
├── README.md
├── RELEASING.md
├── Settings.bundle
├── Root.plist
└── en.lproj
│ └── Root.strings
├── Widgets
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AccentColorDimmed.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── LightIndigo.colorset
│ │ └── Contents.json
│ ├── LiveActivityBackground.colorset
│ │ └── Contents.json
│ ├── WidgetBackground.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
├── BatteryLevel.swift
├── Info.plist
├── MeshActivityAttributes.swift
├── WidgetsBundle.swift
├── WidgetsExtension.entitlements
└── WidgetsLiveActivity.swift
├── ci_scripts
└── ci_pre_xcodebuild.sh
├── meshtastic-1080x1080.png
└── scripts
├── create-release-branch.sh
├── gen_protos.sh
├── hooks
└── pre-commit
├── lint
└── lint-fix-changes.sh
├── setup-hooks.sh
├── thebenternify.sh
└── unthebenternify.sh
/.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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 }}
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "protobufs"]
2 | path = protobufs
3 | url = https://github.com/meshtastic/protobufs.git
4 |
--------------------------------------------------------------------------------
/.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 |
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
--------------------------------------------------------------------------------
/Meshtastic.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Meshtastic.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Meshtastic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Meshtastic.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "a3033aea781828906c453276e3723177901ce64df5757de7ada28c854c9662eb",
3 | "pins" : [
4 | {
5 | "identity" : "cocoamqtt",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/emqx/CocoaMQTT",
8 | "state" : {
9 | "revision" : "aff43422925cc30b9af319f4c4dce4f52859baf4",
10 | "version" : "2.1.8"
11 | }
12 | },
13 | {
14 | "identity" : "mqttcocoaasyncsocket",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/leeway1208/MqttCocoaAsyncSocket",
17 | "state" : {
18 | "revision" : "ce3e18607fd01079495f86ff6195d8a3ca469f73",
19 | "version" : "1.0.8"
20 | }
21 | },
22 | {
23 | "identity" : "starscream",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/daltoniam/Starscream.git",
26 | "state" : {
27 | "revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
28 | "version" : "4.0.8"
29 | }
30 | },
31 | {
32 | "identity" : "swift-protobuf",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/apple/swift-protobuf.git",
35 | "state" : {
36 | "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
37 | "version" : "1.29.0"
38 | }
39 | }
40 | ],
41 | "version" : 3
42 | }
43 |
--------------------------------------------------------------------------------
/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 var title: LocalizedStringResource = "Add Contact"
13 | static var 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 !BLEManager.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 decodedData = Data(base64Encoded: decodedString) {
31 | do {
32 | let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData)
33 | if !success {
34 | throw AppIntentErrors.AppIntentError.message("Failed to add contact")
35 | }
36 |
37 | } catch {
38 | throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)")
39 | }
40 | }
41 | }
42 | // Return a success result
43 | return .result()
44 | } else {
45 | throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link")
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/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/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 var title: LocalizedStringResource = "Disconnect Node"
13 |
14 | static var description: IntentDescription = "Disconnect the currently connected node"
15 |
16 | func perform() async throws -> some IntentResult {
17 | if !BLEManager.shared.isConnected {
18 | throw AppIntentErrors.AppIntentError.notConnected
19 | }
20 |
21 | if let connectedPeripheral = BLEManager.shared.connectedPeripheral,
22 | connectedPeripheral.peripheral.state == .connected {
23 | BLEManager.shared.disconnectPeripheral(reconnect: false)
24 | } else {
25 | throw AppIntentErrors.AppIntentError.message("Error disconnecting node")
26 | }
27 |
28 | return .result()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Meshtastic/AppIntents/FactoryResetNodeIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FactoryResetNodeIntent.swift
3 | // Meshtastic
4 | //
5 | // Created by Benjamin Faershtein on 8/25/24.
6 | //
7 |
8 | import Foundation
9 | import AppIntents
10 |
11 | struct FactoryResetNodeIntent: AppIntent {
12 | static var title: LocalizedStringResource = "Factory Reset"
13 | static var description: IntentDescription = "Perform a factory reset on the node you are connected to"
14 |
15 | func perform() async throws -> some IntentResult {
16 | // Request user confirmation before performing the factory reset
17 | try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName
18 | .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true))
19 |
20 | // Ensure the node is connected
21 | if !BLEManager.shared.isConnected {
22 | throw AppIntentErrors.AppIntentError.notConnected
23 | }
24 |
25 | // Safely unwrap the connected node information
26 | if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
27 | let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
28 | let fromUser = connectedNode.user,
29 | let toUser = connectedNode.user {
30 |
31 | // Attempt to send a factory reset command, throw an error if it fails
32 | if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) {
33 | throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset")
34 | }
35 | } else {
36 | throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
37 | }
38 | //
39 | return .result()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Meshtastic/AppIntents/MessageChannelIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageChannelIntent.swift
3 | // Meshtastic
4 | //
5 | // Created by Benjamin Faershtein on 8/9/24.
6 | //
7 |
8 | import Foundation
9 | import AppIntents
10 |
11 | struct MessageChannelIntent: AppIntent {
12 | static var title: LocalizedStringResource = "Send a Group Message"
13 |
14 | static var description: IntentDescription = "Send a message to a certain meshtastic channel"
15 |
16 | @Parameter(title: "Message")
17 | var messageContent: String
18 |
19 | @Parameter(title: "Channel", controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7))
20 | var channelNumber: Int
21 |
22 | static var parameterSummary: some ParameterSummary {
23 | Summary("Send \(\.$messageContent) to \(\.$channelNumber)")
24 | }
25 | func perform() async throws -> some IntentResult {
26 | if !BLEManager.shared.isConnected {
27 | throw AppIntentErrors.AppIntentError.notConnected
28 | }
29 |
30 | // Check if channel number is between 1 and 7
31 | guard (0...7).contains(channelNumber) else {
32 | throw $channelNumber.needsValueError("Channel number must be between 0 and 7.")
33 | }
34 |
35 | // Convert messageContent to data and check its length
36 | guard let messageData = messageContent.data(using: .utf8) else {
37 | throw AppIntentErrors.AppIntentError.message("Failed to encode message content")
38 | }
39 |
40 | if messageData.count > 200 {
41 | throw $messageContent.needsValueError("Message content exceeds 200 bytes.")
42 | }
43 |
44 | if !BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) {
45 | throw AppIntentErrors.AppIntentError.message("Failed to send message")
46 | }
47 |
48 | return .result()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/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 !BLEManager.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 | if !BLEManager.shared.sendMessage(message: messageContent, toUserNum: Int64(nodeNumber), channel: 0, isEmoji: false, replyID: 0) {
40 | throw AppIntentErrors.AppIntentError.message("Failed to send message")
41 | }
42 |
43 | return .result()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Meshtastic/AppIntents/NavigateToNodeIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigateToNodeIntent.swift
3 | // Meshtastic
4 | //
5 | // Created by Benjamin Faershtein on 2/8/25.
6 | //
7 |
8 | import Foundation
9 | import AppIntents
10 | import CoreLocation
11 | import CoreData
12 | import UIKit
13 |
14 | @available(iOS 16.4, *)
15 | struct NavigateToNodeIntent: ForegroundContinuableIntent {
16 |
17 | static var title: LocalizedStringResource = "Navigate to Node Position"
18 | static var openAppWhenRun: Bool = false
19 |
20 | @Parameter(title: "Node Number")
21 | var nodeNum: Int
22 |
23 | @MainActor
24 | func perform() async throws -> some IntentResult & ProvidesDialog {
25 | if !BLEManager.shared.isConnected {
26 | throw AppIntentErrors.AppIntentError.notConnected
27 | }
28 |
29 | let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity")
30 | fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
31 |
32 | do {
33 | guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity],
34 | fetchedNode.count == 1 else {
35 | throw $nodeNum.needsValueError("Could not find node")
36 | }
37 |
38 | let nodeInfo = fetchedNode[0]
39 | if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
40 | let longitude = nodeInfo.latestPosition?.coordinate.longitude {
41 |
42 | let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)")
43 |
44 | if let mapURL = url, UIApplication.shared.canOpenURL(mapURL) {
45 | // Request to continue in foreground before opening the app
46 | try await requestToContinueInForeground()
47 |
48 | // Open Apple Maps for navigation
49 | UIApplication.shared.open(mapURL, options: [:], completionHandler: nil)
50 | return .result(dialog: "Navigating to node location.")
51 | } else {
52 | throw AppIntentErrors.AppIntentError.message("Unable to open Apple Maps.")
53 | }
54 | } else {
55 | throw AppIntentErrors.AppIntentError.message("Node does not have a recorded position.")
56 | }
57 | } catch {
58 | throw AppIntentErrors.AppIntentError.message("Failed to fetch node data.")
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Meshtastic/AppIntents/NodePositionIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePositionIntent.swift
3 | // Meshtastic
4 | //
5 | // Created by Benjamin Faershtein on 8/10/24.
6 | //
7 |
8 | import Foundation
9 | import AppIntents
10 | import CoreLocation
11 | import CoreData
12 |
13 | struct NodePositionIntent: AppIntent {
14 |
15 | @Parameter(title: "Node Number")
16 | var nodeNum: Int
17 |
18 | static var title: LocalizedStringResource = "Get Node Position"
19 | static var description: IntentDescription = "Fetch the latest position of a cetain node"
20 |
21 | func perform() async throws -> some IntentResult & ReturnsValue {
22 | if !BLEManager.shared.isConnected {
23 | throw AppIntentErrors.AppIntentError.notConnected
24 | }
25 | let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity")
26 | fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
27 | do {
28 | guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else {
29 | throw $nodeNum.needsValueError("Could not find node")
30 | }
31 | let nodeInfo = fetchedNode[0]
32 | if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
33 | let longitude = nodeInfo.latestPosition?.coordinate.longitude {
34 | let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
35 | // Reverse geocode the CLLocation to get a CLPlacemark
36 | let geocoder = CLGeocoder()
37 | let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation)
38 |
39 | if let placemark = placemarks.first {
40 | return .result(value: placemark)
41 | } else {
42 | throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location")
43 | }
44 | } else {
45 | throw AppIntentErrors.AppIntentError.message("Node does not have positions")
46 | }
47 | } catch {
48 | throw AppIntentErrors.AppIntentError.message("Fetch Failure")
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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 var title: LocalizedStringResource = "Restart"
13 |
14 | static var description: IntentDescription = "Restart to the node you are connected to"
15 |
16 | func perform() async throws -> some IntentResult {
17 |
18 | try await requestConfirmation(result: .result(dialog: "Reboot node?"))
19 |
20 | if !BLEManager.shared.isConnected {
21 | throw AppIntentErrors.AppIntentError.notConnected
22 | }
23 | // Safely unwrap the connectedNode using if let
24 | if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
25 | let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
26 | let fromUser = connectedNode.user,
27 | let toUser = connectedNode.user,
28 | let adminIndex = connectedNode.myInfo?.adminIndex {
29 |
30 | // Attempt to send shutdown, throw an error if it fails
31 | if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
32 | throw AppIntentErrors.AppIntentError.message("Failed to restart")
33 | }
34 | } else {
35 | throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
36 | }
37 | return .result()
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/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 var title: LocalizedStringResource = "Shut Down"
13 |
14 | static var 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 !BLEManager.shared.isConnected {
20 | throw AppIntentErrors.AppIntentError.notConnected
21 | }
22 |
23 | // Safely unwrap the connectedNode using if let
24 | if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
25 | let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
26 | let fromUser = connectedNode.user,
27 | let toUser = connectedNode.user,
28 | let adminIndex = connectedNode.myInfo?.adminIndex {
29 |
30 | // Attempt to send shutdown, throw an error if it fails
31 | if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
32 | throw AppIntentErrors.AppIntentError.message("Failed to shut down")
33 | }
34 | } else {
35 | throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
36 | }
37 | return .result()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Meshtastic/AppState.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | class AppState: ObservableObject {
5 | @Published
6 | var router: Router
7 |
8 | @Published
9 | var unreadChannelMessages: Int
10 |
11 | @Published
12 | var unreadDirectMessages: Int
13 |
14 | var totalUnreadMessages: Int {
15 | unreadChannelMessages + unreadDirectMessages
16 | }
17 |
18 | private var cancellables: Set = []
19 |
20 | init(router: Router) {
21 | self.router = router
22 | self.unreadChannelMessages = 0
23 | self.unreadDirectMessages = 0
24 |
25 | // Keep app icon badge count in sync with messages read status
26 | $unreadChannelMessages.combineLatest($unreadDirectMessages)
27 | .sink(receiveValue: { badgeCounts in
28 | UNUserNotificationCenter.current()
29 | .setBadgeCount(badgeCounts.0 + badgeCounts.1)
30 | })
31 | .store(in: &cancellables)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/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/ANDROIDSIM.imageset/play_store_icon_114px-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/ANDROIDSIM.imageset/play_store_icon_114px-4.png
--------------------------------------------------------------------------------
/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/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/Assets.xcassets/AppIcon.appiconset/logo-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-3.png
--------------------------------------------------------------------------------
/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png
--------------------------------------------------------------------------------
/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png
--------------------------------------------------------------------------------
/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/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/NANOG1.imageset/2022-04-01T18-01-04.120Z-meshtastic_mesh_device_nano_edition_G1_P1 1.png
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/SOLAR_NODE.imageset/solar_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.png
--------------------------------------------------------------------------------
/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/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/STATIONG1.imageset/meshtastic_mesh_device_station_edition_overview 1.png
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/UNPHONE.imageset/UNPHONE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Assets.xcassets/UNPHONE.imageset/UNPHONE.png
--------------------------------------------------------------------------------
/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/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/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/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-black.imageset/Mesh_Logo_Black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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/Assets.xcassets/logo-white.imageset/Mesh_Logo_White.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 var 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/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/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 |
--------------------------------------------------------------------------------
/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/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/Extensions/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color.swift
3 | // Meshtastic
4 | //
5 | // Created by Garth Vander Houwen on 4/25/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import UIKit
11 |
12 | extension Color {
13 | /// Returns a boolean for a SwiftUI Color to determine what color of text to use
14 | /// - Returns: true if the color is light
15 | func isLight() -> Bool {
16 | guard let components = cgColor?.components, components.count > 2 else {return false}
17 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
18 | return (brightness > 0.5)
19 | }
20 | public static let magenta = Color(red: 0.50, green: 0.00, blue: 0.00)
21 | }
22 |
23 | extension UIColor {
24 | /// Returns a boolean indicating if a color is light
25 | /// - Returns: true if the color is light
26 | func isLight() -> Bool {
27 | guard let components = cgColor.components, components.count > 2 else {return false}
28 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
29 | return (brightness > 0.5)
30 | }
31 | /// Returns a UInt32 from a UIColor
32 | /// - Returns: UInt32
33 | var hex: UInt32 {
34 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
35 | getRed(&red, green: &green, blue: &blue, alpha: &alpha)
36 | var value: UInt32 = 0
37 | value += UInt32(1.0 * 255) << 24
38 | value += UInt32(red * 255) << 16
39 | value += UInt32(green * 255) << 8
40 | value += UInt32(blue * 255)
41 | return value
42 | }
43 | /// Returns a UIColor from a UInt32 value
44 | /// - Parameter hex: UInt32 value to convert to a color
45 | /// - Returns: UIColor
46 | convenience init(hex: UInt32) {
47 | let red = CGFloat((hex & 0xFF0000) >> 16)
48 | let green = CGFloat((hex & 0x00FF00) >> 8)
49 | let blue = CGFloat((hex & 0x0000FF))
50 | self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/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/Extensions/CoreData/ChannelEntityExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChannelEntityExtension.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 11/7/22.
6 | //
7 | import Foundation
8 | import CoreData
9 | import MeshtasticProtobufs
10 |
11 | extension ChannelEntity {
12 |
13 | var allPrivateMessages: [MessageEntity] {
14 | let context = PersistenceController.shared.container.viewContext
15 | let fetchRequest = MessageEntity.fetchRequest()
16 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)]
17 | fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index)
18 |
19 | return (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
20 | }
21 |
22 | var unreadMessages: Int {
23 |
24 | let unreadMessages = allPrivateMessages.filter { ($0 as AnyObject).read == false }
25 | return unreadMessages.count
26 | }
27 |
28 | var protoBuf: Channel {
29 | var channel = Channel()
30 | channel.index = self.index
31 | channel.settings.name = self.name ?? ""
32 | channel.settings.psk = self.psk ?? Data()
33 | channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary
34 | channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision)
35 | return channel
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/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/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import MeshtasticProtobufs
3 |
4 | extension ExternalNotificationConfigEntity {
5 | convenience init(
6 | context: NSManagedObjectContext,
7 | config: ModuleConfig.ExternalNotificationConfig
8 | ) {
9 | self.init(context: context)
10 | self.enabled = config.enabled
11 | self.usePWM = config.usePwm
12 | self.alertBell = config.alertBell
13 | self.alertBellBuzzer = config.alertBellBuzzer
14 | self.alertBellVibra = config.alertBellVibra
15 | self.alertMessage = config.alertMessage
16 | self.alertMessageBuzzer = config.alertMessageBuzzer
17 | self.alertMessageVibra = config.alertMessageVibra
18 | self.active = config.active
19 | self.output = Int32(config.output)
20 | self.outputBuzzer = Int32(config.outputBuzzer)
21 | self.outputVibra = Int32(config.outputVibra)
22 | self.outputMilliseconds = Int32(config.outputMs)
23 | self.nagTimeout = Int32(config.nagTimeout)
24 | self.useI2SAsBuzzer = config.useI2SAsBuzzer
25 | }
26 |
27 | func update(with config: ModuleConfig.ExternalNotificationConfig) {
28 | enabled = config.enabled
29 | usePWM = config.usePwm
30 | alertBell = config.alertBell
31 | alertBellBuzzer = config.alertBellBuzzer
32 | alertBellVibra = config.alertBellVibra
33 | alertMessage = config.alertMessage
34 | alertMessageBuzzer = config.alertMessageBuzzer
35 | alertMessageVibra = config.alertMessageVibra
36 | active = config.active
37 | output = Int32(config.output)
38 | outputBuzzer = Int32(config.outputBuzzer)
39 | outputVibra = Int32(config.outputVibra)
40 | outputMilliseconds = Int32(config.outputMs)
41 | nagTimeout = Int32(config.nagTimeout)
42 | useI2SAsBuzzer = config.useI2SAsBuzzer
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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/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/Extensions/CoreData/MessageEntityExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageEntityExtension.swift
3 | // Meshtastic
4 | //
5 | // Created by Ben on 8/22/23.
6 | //
7 |
8 | import Foundation
9 |
10 | import CoreData
11 | import CoreLocation
12 | import MapKit
13 | import SwiftUI
14 |
15 | extension MessageEntity {
16 | var timestamp: Date {
17 | let time = messageTimestamp
18 | return Date(timeIntervalSince1970: TimeInterval(time))
19 | }
20 |
21 | var canRetry: Bool {
22 | let re = RoutingError(rawValue: Int(ackError))
23 | return re?.canRetry ?? false
24 | }
25 |
26 | var tapbacks: [MessageEntity] {
27 | let context = PersistenceController.shared.container.viewContext
28 | let fetchRequest = MessageEntity.fetchRequest()
29 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)]
30 | fetchRequest.predicate = NSPredicate(format: "replyID == %lld AND isEmoji == true", self.messageId)
31 |
32 | return (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
33 | }
34 |
35 | func displayTimestamp(aboveMessage: MessageEntity?) -> Bool {
36 | if let aboveMessage = aboveMessage {
37 | return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes
38 | }
39 | return false // First message will have no timestamp
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/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 |
10 | extension MyInfoEntity {
11 |
12 | var messageList: [MessageEntity] {
13 | let context = PersistenceController.shared.container.viewContext
14 | let fetchRequest = MessageEntity.fetchRequest()
15 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)]
16 | fetchRequest.predicate = NSPredicate(format: "toUser == nil")
17 |
18 | return (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
19 | }
20 |
21 | var unreadMessages: Int {
22 | let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
23 | return unreadMessages.count
24 | }
25 |
26 | var hasAdmin: Bool {
27 | let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" }
28 | return adminChannel?.count ?? 0 > 0
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift:
--------------------------------------------------------------------------------
1 | // NodeInfoEntityToNodeInfo.swift
2 | // Meshtastic
3 | //
4 | // Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf)
5 |
6 | import Foundation
7 | import MeshtasticProtobufs
8 |
9 | extension NodeInfoEntity {
10 | func toProto() -> NodeInfo {
11 | var userProto = User()
12 | if let user = self.user {
13 | userProto.id = user.userId ?? ""
14 | userProto.longName = user.longName ?? ""
15 | userProto.shortName = user.shortName ?? ""
16 | userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset
17 | userProto.isLicensed = user.isLicensed
18 | if userProto.hasIsUnmessagable == true {
19 | userProto.isUnmessagable = user.unmessagable
20 | }
21 | userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client
22 | userProto.publicKey = user.publicKey?.subdata(in: 0.. NSFetchRequest {
15 | let request: NSFetchRequest = WaypointEntity.fetchRequest()
16 | request.fetchLimit = 50
17 | // request.fetchBatchSize = 1
18 | // request.returnsObjectsAsFaults = false
19 | // request.includesSubentities = true
20 | request.returnsDistinctResults = true
21 | request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
22 | request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate)
23 | return request
24 | }
25 |
26 | var latitude: Double? {
27 |
28 | let d = Double(latitudeI)
29 | if d == 0 {
30 | return 0
31 | }
32 | return d / 1e7
33 | }
34 |
35 | var longitude: Double? {
36 |
37 | let d = Double(longitudeI)
38 | if d == 0 {
39 | return 0
40 | }
41 | return d / 1e7
42 | }
43 |
44 | var waypointCoordinate: CLLocationCoordinate2D? {
45 | if latitudeI != 0 && longitudeI != 0 {
46 | let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
47 | return coord
48 | } else {
49 | return nil
50 | }
51 | }
52 |
53 | var annotaton: MKPointAnnotation {
54 | let pointAnn = MKPointAnnotation()
55 | if waypointCoordinate != nil {
56 | pointAnn.coordinate = waypointCoordinate!
57 | }
58 | return pointAnn
59 | }
60 | }
61 |
62 | extension WaypointEntity: MKAnnotation {
63 | public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationsHandler.DefaultLocation }
64 | public var title: String? { name ?? "Dropped Pin" }
65 | public var subtitle: String? {
66 | (longDescription ?? "") +
67 | String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") +
68 | String(locked > 0 ? "\n🔒 Locked" : "") }
69 | }
70 |
71 | struct WaypointCoordinate: Identifiable {
72 |
73 | let id: UUID
74 | let coordinate: CLLocationCoordinate2D?
75 | let waypointId: Int64
76 | }
77 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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: "!%2X", self).lowercased()
22 | }
23 | }
24 |
25 | extension Int64 {
26 | func toHex() -> String {
27 | return String(format: "!%2X", self).lowercased()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Meshtastic/Extensions/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 6/3/24.
6 | //
7 |
8 | import OSLog
9 |
10 | extension Logger {
11 |
12 | /// The logger's subsystem.
13 | private static var subsystem = Bundle.main.bundleIdentifier!
14 |
15 | /// All admin messages
16 | static let admin = Logger(subsystem: subsystem, category: "🏛 Admin")
17 |
18 | /// All logs related to data such as decoding error, parsing issues, etc.
19 | static let data = Logger(subsystem: subsystem, category: "🗄️ Data")
20 |
21 | /// All logs related to the mesh
22 | static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh")
23 |
24 | /// All logs related to MQTT
25 | static let mqtt = Logger(subsystem: subsystem, category: "📱 MQTT")
26 |
27 | /// All detailed logs originating from the device (radio).
28 | static let radio = Logger(subsystem: subsystem, category: "📟 Radio")
29 |
30 | /// All logs related to services such as network calls, location, etc.
31 | static let services = Logger(subsystem: subsystem, category: "🍏 Services")
32 |
33 | /// All logs related to tracking and analytics.
34 | static let statistics = Logger(subsystem: subsystem, category: "📊 Stats")
35 |
36 | /// Fetch from the logstore
37 | static public func fetch(predicateFormat: String) async throws -> [OSLogEntryLog] {
38 |
39 | let store = try OSLogStore(scope: .currentProcessIdentifier)
40 | let position = store.position(timeIntervalSinceLatestBoot: 0)
41 | // let calendar = Calendar.current
42 | // let dayAgo = calendar.date(byAdding: .day, value: -1, to: Date.now)
43 | // let position = store.position(date: dayAgo!)
44 | let predicate = NSPredicate(format: predicateFormat)
45 | let entries = try store.getEntries(at: position, matching: predicate)
46 |
47 | var logs: [OSLogEntryLog] = []
48 | for entry in entries {
49 |
50 | try Task.checkCancellation()
51 |
52 | if let log = entry as? OSLogEntryLog {
53 | logs.append(log)
54 | }
55 | }
56 |
57 | if logs.isEmpty { logs = [] }
58 | return logs
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/Extensions/Url.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Url.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 5/5/23.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | extension URL {
12 |
13 | func regularFileAllocatedSize() throws -> UInt64 {
14 | let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
15 |
16 | guard resourceValues.isRegularFile ?? false else {
17 | return 0
18 | }
19 | return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
20 | }
21 | subscript(queryParam: String) -> String? {
22 | guard let url = URLComponents(string: self.absoluteString) else { return nil }
23 | if let parameters = url.queryItems {
24 | return parameters.first(where: { $0.name == queryParam })?.value
25 | } else if let paramPairs = url.fragment?.components(separatedBy: "?").last?.components(separatedBy: "&") {
26 | for pair in paramPairs where pair.contains(queryParam) {
27 | return pair.components(separatedBy: "=").last
28 | }
29 | return nil
30 | } else {
31 | return nil
32 | }
33 | }
34 | var attributes: [FileAttributeKey: Any]? {
35 | do {
36 | return try FileManager.default.attributesOfItem(atPath: path)
37 | } catch let error as NSError {
38 | Logger.services.error("FileAttribute error: \(error, privacy: . public)")
39 | }
40 | return nil
41 | }
42 |
43 | var fileSize: UInt64 {
44 | return attributes?[.size] as? UInt64 ?? UInt64(0)
45 | }
46 |
47 | var fileSizeString: String {
48 | return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
49 | }
50 |
51 | var creationDate: Date? {
52 | return attributes?[.creationDate] as? Date
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Meshtastic/Extensions/View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 8/14/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension View {
11 | func onFirstAppear(_ action: @escaping () -> Void) -> some View {
12 | modifier(FirstAppear(action: action))
13 | }
14 | }
15 |
16 | private struct FirstAppear: ViewModifier {
17 | let action: () -> Void
18 |
19 | // Use this to only fire your block one time
20 | @State private var hasAppeared = false
21 |
22 | func body(content: Content) -> some View {
23 | // And then, track it here
24 | content.onAppear {
25 | guard !hasAppeared else { return }
26 | hasAppeared = true
27 | action()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/Meshtastic/Meshtastic.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.usernotifications.critical-alerts
6 |
7 | com.apple.developer.associated-domains
8 |
9 | applinks:meshtastic.org/e/*
10 | applinks:meshtastic.org/v/*
11 |
12 | com.apple.developer.weatherkit
13 |
14 | com.apple.security.app-sandbox
15 |
16 | com.apple.security.device.bluetooth
17 |
18 | com.apple.security.files.user-selected.read-write
19 |
20 | com.apple.security.network.client
21 |
22 | com.apple.security.personal-information.location
23 |
24 | com.apple.developer.carplay-communication
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | MeshtasticDataModelV 51.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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/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/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/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/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/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120-1.png
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/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/Resources/Assets.xcassets/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6-2.png
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/HELTECV20.imageset/655DCEC0-309D-430A-AF50-2453B6ADB1F6.png
--------------------------------------------------------------------------------
/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/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/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/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/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/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
--------------------------------------------------------------------------------
/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/tbeam.imageset/tbeam-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam-2.jpg
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tbeam.imageset/tbeam.jpg
--------------------------------------------------------------------------------
/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/techo.imageset/techo-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo-2.jpg
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/techo.imageset/techo.jpg
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/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/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora-2.jpeg
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/Assets.xcassets/tlorav1.imageset/tlora.jpeg
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam-1.jpg
--------------------------------------------------------------------------------
/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/Assets.xcassets/TBEAM.imageset/tbeam.jpg
--------------------------------------------------------------------------------
/Meshtastic/Resources/alpha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Meshtastic/Resources/alpha.png
--------------------------------------------------------------------------------
/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 bluetooth
61 | case nodes
62 | case map
63 | case settings
64 | }
65 |
66 | var selectedTab: Tab = .bluetooth
67 | var messages: MessagesNavigationState?
68 | var nodeListSelectedNodeNum: Int64?
69 | var map: MapNavigationState?
70 | var settings: SettingsNavigationState?
71 | }
72 |
--------------------------------------------------------------------------------
/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 BluetoothConnectionTip: Tip {
11 |
12 | var id: String {
13 | return "tip.bluetooth.connect"
14 | }
15 | var title: Text {
16 | Text("Connected Radio")
17 | }
18 | var message: Text? {
19 | Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.")
20 | }
21 | var image: Image? {
22 | Image(systemName: "flipphone")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Bluetooth/InvalidVersion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InvalidVersion.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 7/13/22.
6 | //
7 | import SwiftUI
8 |
9 | struct InvalidVersion: View {
10 |
11 | @Environment(\.dismiss) private var dismiss
12 |
13 | @State var minimumVersion = ""
14 | @State var version = ""
15 |
16 | var body: some View {
17 |
18 | VStack {
19 |
20 | Text("Update Your Firmware")
21 | .font(.largeTitle)
22 | .foregroundColor(.orange)
23 |
24 | Divider()
25 | VStack {
26 | Text("The Meshtastic Apple apps support firmware version \(minimumVersion) and above.")
27 | .font(.title2)
28 | .padding(.bottom)
29 | Link("Firmware update docs", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/")!)
30 | .font(.title)
31 | .padding()
32 | Link("Additional help", destination: URL(string: "https://meshtastic.org/docs/faq")!)
33 | .font(.title)
34 | .padding()
35 | }
36 | .padding()
37 | Divider()
38 | .padding(.top)
39 | VStack {
40 | Text("🦕 End of life Version 🦖 ☄️")
41 | .font(.title3)
42 | .foregroundColor(.orange)
43 | .padding(.bottom)
44 | Text("Version \(minimumVersion) includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version \(minimumVersion) and above are supported.")
45 | .font(.callout)
46 | .padding([.leading, .trailing, .bottom])
47 |
48 | #if targetEnvironment(macCatalyst)
49 | Button {
50 | dismiss()
51 | } label: {
52 | Label("Close", systemImage: "xmark")
53 |
54 | }
55 | .buttonStyle(.bordered)
56 | .buttonBorderShape(.capsule)
57 | .controlSize(.large)
58 | .padding()
59 | #endif
60 |
61 | }.padding()
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Meshtastic/Views/ContentView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) Garth Vander Houwen 2021
3 | */
4 |
5 | import SwiftUI
6 |
7 | struct ContentView: View {
8 | @ObservedObject
9 | var appState: AppState
10 |
11 | @ObservedObject
12 | var router: Router
13 |
14 | init(appState: AppState, router: Router) {
15 | self.appState = appState
16 | self.router = router
17 | UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified)
18 | }
19 |
20 | var body: some View {
21 | TabView(selection: $appState.router.navigationState.selectedTab) {
22 | Messages(
23 | router: appState.router,
24 | unreadChannelMessages: $appState.unreadChannelMessages,
25 | unreadDirectMessages: $appState.unreadDirectMessages
26 | )
27 | .tabItem {
28 | Label("Messages", systemImage: "message")
29 | }
30 | .tag(NavigationState.Tab.messages)
31 | .badge(appState.totalUnreadMessages)
32 |
33 | Connect()
34 | .tabItem {
35 | Label("Bluetooth", systemImage: "antenna.radiowaves.left.and.right")
36 | }
37 | .tag(NavigationState.Tab.bluetooth)
38 |
39 | NodeList(
40 | router: appState.router
41 | )
42 | .tabItem {
43 | Label("Nodes", systemImage: "flipphone")
44 | }
45 | .tag(NavigationState.Tab.nodes)
46 |
47 | MeshMap(router: appState.router)
48 | .tabItem {
49 | Label("Mesh Map", systemImage: "map")
50 | }
51 | .tag(NavigationState.Tab.map)
52 |
53 | Settings(
54 | router: appState.router
55 | )
56 | .tabItem {
57 | Label("Settings", systemImage: "gear")
58 | .font(.title)
59 | }
60 | .tag(NavigationState.Tab.settings)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/Compact Widgets/SoilCompactWidgets.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoilCompactWidgets.swift
3 | // Meshtastic
4 | //
5 | // Created by Jake Bordens on 3/14/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SoilTemperatureCompactWidget: View {
11 | let temperature: String
12 | let unit: String
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | HStack(alignment: .firstTextBaseline) {
17 | Image("soil.temperature")
18 | .imageScale(.small)
19 | .foregroundColor(.accentColor)
20 | Text("Soil Temp")
21 | .textCase(.uppercase)
22 | .font(.callout)
23 | }
24 | HStack {
25 | Text("\(temperature)")
26 | .font(temperature.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 | struct SoilMoistureCompactWidget: View {
38 | let moisture: String
39 | let unit: String
40 |
41 | var body: some View {
42 | VStack(alignment: .leading) {
43 | HStack(alignment: .firstTextBaseline) {
44 | Image("soil.moisture")
45 | .imageScale(.small)
46 | .foregroundColor(.accentColor)
47 | Text("Soil Moisture")
48 | .textCase(.uppercase)
49 | .font(.callout)
50 | }
51 | HStack {
52 | Text("\(moisture)")
53 | .font(moisture.length < 4 ? .system(size: 50) : .system(size: 40) )
54 | Text(unit)
55 | .font(.system(size: 14))
56 | }
57 | }
58 | .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
59 | .padding()
60 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
61 | }
62 | }
63 |
64 | #Preview {
65 | let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
66 | Form {
67 | LazyVGrid(columns: gridItemLayout) {
68 | SoilTemperatureCompactWidget(temperature: "23", unit: "°C")
69 | SoilMoistureCompactWidget(moisture: "23", unit: "%")
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/Views/Helpers/Help/DirectMessagesHelp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DirectMessagesHelp.swift
3 | // Meshtastic
4 | //
5 | // Copyright Garth Vander Houwen on 8/15/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DirectMessagesHelp: View {
11 | private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
12 | @Environment(\.dismiss) private var dismiss
13 |
14 | var body: some View {
15 | ScrollView {
16 | Label("Direct Message Help", systemImage: "questionmark.circle")
17 | .font(.title)
18 | .padding(.vertical)
19 | VStack(alignment: .leading) {
20 | HStack {
21 | Image(systemName: "star.fill")
22 | .foregroundColor(.yellow)
23 | .padding(.bottom)
24 | Text("Favorites and nodes with recent messages show up at the top of the contact list.")
25 | .fixedSize(horizontal: false, vertical: true)
26 | .padding(.bottom)
27 | }
28 | HStack {
29 | Image(systemName: "hand.tap")
30 | .padding(.bottom)
31 | Text("Long press to favorite or mute the contact or delete a conversation.")
32 | .fixedSize(horizontal: false, vertical: true)
33 | .padding(.bottom)
34 | }
35 | }
36 | if idiom == .phone {
37 | VStack(alignment: .leading) {
38 | LockLegend()
39 | AckErrors()
40 | }
41 | } else {
42 | HStack(alignment: .top) {
43 | LockLegend()
44 | AckErrors()
45 | .padding(.trailing)
46 | }
47 | }
48 | #if targetEnvironment(macCatalyst)
49 | Spacer()
50 | Button {
51 | dismiss()
52 | } label: {
53 | Label("Close", systemImage: "xmark")
54 | }
55 | .buttonStyle(.bordered)
56 | .buttonBorderShape(.capsule)
57 | .controlSize(.large)
58 | .padding(.bottom)
59 | #endif
60 | }
61 | .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading)
62 | .padding()
63 | .presentationDetents([.large])
64 | .presentationContentInteraction(.scrolls)
65 | .presentationDragIndicator(.visible)
66 | .presentationBackgroundInteraction(.enabled(upThrough: .large))
67 | }
68 | }
69 |
70 | struct DirectMessagesHelpPreviews: PreviewProvider {
71 | static var previews: some View {
72 | VStack {
73 | AckErrors()
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Helpers/MQTTIcon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MQTTIcon.swift
3 | // Meshtastic
4 | //
5 | // Created by Matthew Davies on 4/1/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct MQTTIcon: View {
12 | var connected: Bool = false
13 | var uplink: Bool = false
14 | var downlink: Bool = false
15 | var topic: String = ""
16 |
17 | @State var isPopoverOpen = false
18 |
19 | var body: some View {
20 | Button( action: {
21 | if topic.length > 0 {self.isPopoverOpen.toggle()}
22 | }) {
23 | // the last one defaults to just showing up/down if it isn't specified b/c on the mqtt config screen, there's no information about uplink/downlink and no good alternative icon
24 | Image(systemName: uplink && downlink ? "arrow.up.arrow.down.circle.fill" : uplink ? "arrow.up.circle.fill" : downlink ? "arrow.down.circle.fill" : "arrow.up.arrow.down.circle.fill")
25 | .imageScale(.large)
26 | .foregroundColor(connected ? .green : .secondary)
27 | .symbolRenderingMode(.hierarchical)
28 | }.popover(isPresented: self.$isPopoverOpen, arrowEdge: .bottom, content: {
29 | VStack(spacing: 0.5) {
30 | Text("Topic: \(topic)".localized)
31 | .padding(20)
32 | Button("Close", action: { self.isPopoverOpen = false }).padding([.bottom], 20)
33 | }
34 | .presentationCompactAdaptation(.popover)
35 | })
36 | }
37 | }
38 |
39 | struct MQTTIcon_Previews: PreviewProvider {
40 | static var previews: some View {
41 | VStack {
42 | MQTTIcon(connected: true)
43 | MQTTIcon(connected: false)
44 |
45 | MQTTIcon(connected: true, uplink: true, downlink: true)
46 | MQTTIcon(connected: false, uplink: true, downlink: true)
47 |
48 | MQTTIcon(connected: true, uplink: true)
49 | MQTTIcon(connected: false, uplink: true)
50 |
51 | MQTTIcon(connected: true, downlink: true)
52 | MQTTIcon(connected: false, downlink: true)
53 | }.previewLayout(.fixed(width: 25, height: 220))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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 |
15 | #if targetEnvironment(macCatalyst)
16 | VStack {
17 | Image("logo-white")
18 | .resizable()
19 | .renderingMode(.template)
20 | .foregroundColor(.accentColor)
21 | .scaledToFit()
22 | }
23 | .padding(.bottom, 5)
24 | .padding(.top, 5)
25 | .offset(x: -15)
26 | #else
27 | VStack {
28 | Image(colorScheme == .dark ? "logo-white" : "logo-black")
29 | .resizable()
30 | .renderingMode(.template)
31 | .scaledToFit()
32 | }
33 | .padding(.bottom, 5)
34 | .offset(x: -15)
35 | #endif
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/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/Views/Helpers/SecureInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecureInput.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 8/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SecureInput: View {
11 |
12 | private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
13 | @Binding private var text: String
14 | @Binding private var isValid: Bool
15 | @State var isSecure: Bool = true
16 | private var title: String
17 |
18 | init(_ title: String, text: Binding, isValid: Binding) {
19 | self.title = title
20 | self._text = text
21 | self._isValid = isValid
22 | }
23 |
24 | var body: some View {
25 | ZStack(alignment: .trailing) {
26 | Group {
27 | if isSecure {
28 | SecureField(title, text: $text)
29 | .font(idiom == .phone ? .caption : .callout)
30 | .allowsTightening(true)
31 | .monospaced()
32 | .keyboardType(.alphabet)
33 | .foregroundStyle(.tertiary)
34 | .disableAutocorrection(true)
35 | } else {
36 | TextField(title, text: $text, axis: .vertical)
37 | .font(idiom == .phone ? .caption : .callout)
38 | .allowsTightening(true)
39 | .monospaced()
40 | .keyboardType(.alphabet)
41 | .foregroundStyle(.tertiary)
42 | .disableAutocorrection(true)
43 | .textSelection(.enabled)
44 | .lineLimit(...3)
45 | .background(
46 | RoundedRectangle(cornerRadius: 10.0)
47 | .stroke(isValid ? Color.clear : Color.red, lineWidth: 2.0)
48 | )
49 | }
50 | }.padding(.trailing, 36)
51 |
52 | if !text.isEmpty {
53 | Button(action: {
54 | isSecure.toggle()
55 | }) {
56 | Image(systemName: self.isSecure ? "eye.slash" : "eye")
57 | .accentColor(.secondary)
58 | }.buttonStyle(BorderlessButtonStyle())
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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/Views/Layouts/TraceRoute.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TraceRoute.swift
3 | // Meshtastic
4 | //
5 | // Created by Garth Vander Houwen on 9/22/24.
6 | //
7 | import SwiftUI
8 |
9 | struct Rotation: LayoutValueKey {
10 | static let defaultValue: Binding? = nil
11 | }
12 |
13 | struct TraceRouteComponent: View {
14 | var animation: Animation?
15 | @ViewBuilder let content: () -> V
16 | @State private var rotation: Angle = .zero
17 |
18 | var body: some View {
19 | content()
20 | .rotationEffect(rotation)
21 | .layoutValue(key: Rotation.self, value: $rotation.animation(animation))
22 | }
23 | }
24 |
25 | struct TraceRoute: Layout {
26 | var animatableData: AnimatablePair {
27 | get {
28 | AnimatablePair(rotation.radians, radius)
29 | }
30 | set {
31 | rotation = Angle.radians(newValue.first)
32 | radius = newValue.second
33 | }
34 | }
35 |
36 | var radius: CGFloat
37 | var rotation: Angle
38 |
39 | func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
40 | let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) {
41 | return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height))
42 | }
43 | return CGSize(width: (maxSize.width / 2 + radius) * 2,
44 | height: (maxSize.height / 2 + radius) * 2)
45 | }
46 |
47 | func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
48 | let angleStep = (Angle.degrees(360).radians / Double(subviews.count))
49 |
50 | for (index, subview) in subviews.enumerated() {
51 | let angle = angleStep * CGFloat(index) + rotation.radians
52 |
53 | var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle))
54 | point.x += bounds.midX
55 | point.y += bounds.midY
56 |
57 | subview.place(at: point, anchor: .center, proposal: .unspecified)
58 |
59 | // DispatchQueue.main.async {
60 | if index % 2 == 0 {
61 | subview[Rotation.self]?.wrappedValue = .zero
62 | } else {
63 | subview[Rotation.self]?.wrappedValue = .radians(angle)
64 | }
65 | // }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Messages/RetryButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import OSLog
3 |
4 | struct RetryButton: View {
5 | @Environment(\.managedObjectContext) var context
6 | @EnvironmentObject var bleManager: BLEManager
7 |
8 | let message: MessageEntity
9 | let destination: MessageDestination
10 | @State var isShowingConfirmation = false
11 |
12 | var body: some View {
13 | Button {
14 | isShowingConfirmation = true
15 | } label: {
16 | Image(systemName: "exclamationmark.circle")
17 | .foregroundColor(.gray)
18 | .frame(height: 30)
19 | .padding(.top, 5)
20 | }
21 | .confirmationDialog(
22 | "This message was likely not delivered.",
23 | isPresented: $isShowingConfirmation,
24 | titleVisibility: .visible
25 | ) {
26 | Button("Try Again") {
27 | guard bleManager.connectedPeripheral?.peripheral.state == .connected else {
28 | return
29 | }
30 | let messageID = message.messageId
31 | let payload = message.messagePayload ?? ""
32 | let userNum = message.toUser?.num ?? 0
33 | let channel = message.channel
34 | let isEmoji = message.isEmoji
35 | let replyID = message.replyID
36 | context.delete(message)
37 | do {
38 | try context.save()
39 | } catch {
40 | Logger.data.error("Failed to delete message \(messageID, privacy: .public): \(error.localizedDescription, privacy: .public)")
41 | }
42 | if !bleManager.sendMessage(
43 | message: payload,
44 | toUserNum: userNum,
45 | channel: channel,
46 | isEmoji: isEmoji,
47 | replyID: replyID
48 | ) {
49 | // Best effort, unlikely since we already checked BLE state
50 | Logger.services.warning("Failed to resend message \(messageID, privacy: .public)")
51 | } else {
52 | switch destination {
53 | case .user:
54 | break
55 | case let .channel(channel):
56 | // We must refresh the channel to trigger a view update since its relationship
57 | // to messages is via a weak fetched property which is not updated by
58 | // `bleManager.sendMessage` unlike the user entity.
59 | context.refresh(channel, mergeChanges: true)
60 | }
61 | }
62 | }
63 | Button("Cancel", role: .cancel) {}
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/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/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/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/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/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ClientHistoryButton: View {
4 | var bleManager: BLEManager
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 | isPresentingAlert = bleManager.requestStoreAndForwardClientHistory(
16 | fromUser: connectedNode.user!,
17 | toUser: node.user!
18 | )
19 | } label: {
20 | Label(
21 | "Client History",
22 | systemImage: "envelope.arrow.triangle.branch"
23 | )
24 | }.alert(
25 | "Client History Request Sent",
26 | isPresented: $isPresentingAlert
27 | ) {
28 | Button("OK") { }.keyboardShortcut(.defaultAction)
29 | } message: {
30 | Text("Any missed messages will be delivered again.")
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import OSLog
3 | import SwiftUI
4 |
5 | struct DeleteNodeButton: View {
6 |
7 | var bleManager: BLEManager
8 | var context: NSManagedObjectContext
9 | var connectedNode: NodeInfoEntity
10 | var node: NodeInfoEntity
11 | @Environment(\.dismiss) private var dismiss
12 | @State private var isPresentingAlert = false
13 |
14 | var body: some View {
15 | if node.num != connectedNode.num {
16 | Button(role: .destructive) {
17 | isPresentingAlert = true
18 | } label: {
19 | Label {
20 | Text("Delete Node")
21 | } icon: {
22 | Image(systemName: "trash")
23 | .symbolRenderingMode(.multicolor)
24 | }
25 | }
26 | .alert(
27 | "Are you sure?",
28 | isPresented: $isPresentingAlert
29 | ) {
30 | Button("OK") { }.keyboardShortcut(.defaultAction)
31 | } message: {
32 | Text("Delete Node?")
33 | }
34 | .confirmationDialog(
35 | "Are you sure?",
36 | isPresented: $isPresentingAlert,
37 | titleVisibility: .visible
38 | ) {
39 | Button("Delete Node", role: .destructive) {
40 | guard let deleteNode = getNodeInfo(
41 | id: node.num,
42 | context: context
43 | ) else {
44 | Logger.data.error("Unable to find node info to delete node \(node.num, privacy: .public)")
45 | return
46 | }
47 | let success = bleManager.removeNode(
48 | node: deleteNode,
49 | connectedNodeNum: connectedNode.num
50 | )
51 | if !success {
52 | Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "Unknown".localized, privacy: .public)")
53 | } else {
54 | dismiss()
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import SwiftUI
3 |
4 | struct ExchangePositionsButton: View {
5 | var bleManager: BLEManager
6 |
7 | var node: NodeInfoEntity
8 |
9 | @State private var isPresentingPositionSentAlert: Bool = false
10 | @State private var isPresentingPositionFailedAlert: Bool = false
11 |
12 | var body: some View {
13 | Button {
14 | let positionSent = bleManager.sendPosition(
15 | channel: node.channel,
16 | destNum: node.num,
17 | wantResponse: true
18 | )
19 | if positionSent {
20 | isPresentingPositionSentAlert = true
21 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
22 | isPresentingPositionSentAlert = false
23 | }
24 | } else {
25 | isPresentingPositionFailedAlert = true
26 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
27 | isPresentingPositionFailedAlert = false
28 | }
29 | }
30 |
31 | } label: {
32 | Label {
33 | Text("Exchange Positions")
34 | } icon: {
35 | Image(systemName: "arrow.triangle.2.circlepath")
36 | .symbolRenderingMode(.hierarchical)
37 | }
38 | }.alert(
39 | "Position Sent",
40 | isPresented: $isPresentingPositionSentAlert
41 | ) {
42 | Button("OK") { }.keyboardShortcut(.defaultAction)
43 | } message: {
44 | Text("Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned.")
45 | }.alert(
46 | "Position Exchange Failed",
47 | isPresented: $isPresentingPositionFailedAlert
48 | ) {
49 | Button("OK") { }.keyboardShortcut(.defaultAction)
50 | } message: {
51 | Text("Failed to get a valid position to exchange.")
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import OSLog
3 | import SwiftUI
4 |
5 | struct FavoriteNodeButton: View {
6 | var bleManager: BLEManager
7 | var context: NSManagedObjectContext
8 |
9 | @ObservedObject
10 | var node: NodeInfoEntity
11 |
12 | var body: some View {
13 | Button {
14 | guard let connectedNodeNum = bleManager.connectedPeripheral?.num else { return }
15 | let success = if node.favorite {
16 | bleManager.removeFavoriteNode(
17 | node: node,
18 | connectedNodeNum: Int64(connectedNodeNum)
19 | )
20 | } else {
21 | bleManager.setFavoriteNode(
22 | node: node,
23 | connectedNodeNum: Int64(connectedNodeNum)
24 | )
25 | }
26 | if success {
27 | node.favorite = !node.favorite
28 | do {
29 | try context.save()
30 | } catch {
31 | context.rollback()
32 | Logger.data.error("Save Node Favorite Error")
33 | }
34 | Logger.data.debug("Favorited a node")
35 | }
36 | } label: {
37 | Label {
38 | Text(node.favorite ? "Remove from favorites" : "Add to favorites")
39 | } icon: {
40 | Image(systemName: node.favorite ? "star.fill" : "star")
41 | .symbolRenderingMode(.multicolor)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import OSLog
3 | import SwiftUI
4 |
5 | struct IgnoreNodeButton: View {
6 | var bleManager: BLEManager
7 | var context: NSManagedObjectContext
8 |
9 | @ObservedObject
10 | var node: NodeInfoEntity
11 |
12 | var body: some View {
13 | Button(role: .destructive) {
14 | guard let connectedNodeNum = bleManager.connectedPeripheral?.num else { return }
15 | let success = if node.ignored {
16 | bleManager.removeIgnoredNode(
17 | node: node,
18 | connectedNodeNum: Int64(connectedNodeNum)
19 | )
20 | } else {
21 | bleManager.setIgnoredNode(
22 | node: node,
23 | connectedNodeNum: Int64(connectedNodeNum)
24 | )
25 | }
26 | if success {
27 | node.ignored = !node.ignored
28 | do {
29 | try context.save()
30 | } catch {
31 | context.rollback()
32 | Logger.data.error("Save Ignored Node Error")
33 | }
34 | Logger.data.debug("Ignored a node")
35 | }
36 | } label: {
37 | Label {
38 | Text(node.ignored ? "Remove from ignored" : "Ignore Node")
39 | } icon: {
40 | Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle")
41 | .symbolRenderingMode(.multicolor)
42 | }
43 | // Accessibility: Label for VoiceOver
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigateToButton.swift
3 | // Meshtastic
4 | //
5 | // Created by Benjamin Faershtein on 2/8/25.
6 | //
7 |
8 | import SwiftUI
9 | import CoreLocation
10 | import CoreData
11 | import OSLog
12 |
13 | struct NavigateToButton: View {
14 | var node: NodeInfoEntity
15 |
16 | var body: some View {
17 | Button {
18 | guard let userNum = node.user?.num else {
19 | Logger.services.error("NavigateToAction: Selected node does not exist")
20 | return
21 | }
22 | Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
23 |
24 | let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity")
25 | fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
26 |
27 | do {
28 | let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
29 | guard let nodeInfo = fetchedNodes.first else {
30 | Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
31 | return
32 | }
33 |
34 | if let latitude = nodeInfo.latestPosition?.latitude,
35 | let longitude = nodeInfo.latestPosition?.longitude {
36 | if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
37 | UIApplication.shared.open(url, options: [:], completionHandler: nil)
38 | } else {
39 | Logger.services.error("Failed to create URL for navigation")
40 | }
41 | } else {
42 | Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
43 | }
44 | } catch {
45 | Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
46 | }
47 | } label: {
48 | Label {
49 | Text("Navigate to node")
50 | } icon: {
51 | Image(systemName: "map")
52 | .symbolRenderingMode(.hierarchical)
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/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/Actions/TraceRouteButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TraceRouteButton: View {
4 | var bleManager: BLEManager
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 | isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest(
14 | destNum: node.user?.num ?? 0,
15 | wantResponse: true
16 | )
17 | } label: { completion in
18 | if let completion, completion.percentComplete > 0.0 {
19 | Label {
20 | Text("Trace Route (in \(completion.secondsRemaining.formatted(.number.precision(.fractionLength(0))))s)")
21 | .foregroundStyle(.secondary)
22 | } icon: {
23 | Image("progress.ring.dashed", variableValue: completion.percentComplete)
24 | .foregroundStyle(.secondary)
25 | }.disabled(true)
26 | } else {
27 | Label {
28 | Text("Trace Route")
29 | } icon: {
30 | Image(systemName: "signpost.right.and.left")
31 | .symbolRenderingMode(.hierarchical)
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Nodes/NodeRow.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct NodeRow: View {
4 | var node: NodeInfoEntity
5 | var connected: Bool
6 |
7 | var body: some View {
8 | VStack(alignment: .leading) {
9 |
10 | HStack {
11 |
12 | CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5)
13 | .offset(x: -15)
14 |
15 | if UIDevice.current.userInterfaceIdiom == .pad {
16 | Text(node.user?.longName ?? "Unknown").font(.headline)
17 | .offset(x: -15)
18 | } else {
19 | Text(node.user?.longName ?? "Unknown").font(.title)
20 | .offset(x: -15)
21 | }
22 | }
23 | .padding(.bottom, 10)
24 |
25 | if connected {
26 | HStack(alignment: .bottom) {
27 |
28 | Image(systemName: "repeat.circle.fill").font(.title3)
29 | .foregroundColor(.accentColor).symbolRenderingMode(.hierarchical)
30 | Text("Currently Connected").font(.title3).foregroundColor(Color.accentColor)
31 | }
32 | Spacer()
33 | }
34 |
35 | HStack(alignment: .bottom) {
36 |
37 | Image(systemName: "clock.badge.checkmark.fill").font(.title3).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical)
38 |
39 | if UIDevice.current.userInterfaceIdiom == .pad {
40 |
41 | if node.lastHeard != nil {
42 | Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.caption).foregroundColor(.gray)
43 | .padding(.bottom)
44 | } else {
45 | Text("Last Heard: Unknown").font(.caption).foregroundColor(.gray)
46 | }
47 |
48 | } else {
49 |
50 | if node.lastHeard != nil {
51 | Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.subheadline).foregroundColor(.gray)
52 | } else {
53 | Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray)
54 | }
55 | }
56 | }
57 | }.padding([.leading, .top, .bottom])
58 | }
59 | }
60 |
61 | struct NodeRow_Previews: PreviewProvider {
62 | // static var nodes = BLEManager().meshData.nodes
63 |
64 | static var previews: some View {
65 | Group {
66 | // NodeRow(node: nodes[0], connected: true)
67 | }
68 | .previewLayout(.fixed(width: 300, height: 70))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Settings/About.swift:
--------------------------------------------------------------------------------
1 | //
2 | // About.swift
3 | // Meshtastic
4 | //
5 | // Copyright(c) Garth Vander Houwen 10/6/22.
6 | //
7 | import SwiftUI
8 | import StoreKit
9 |
10 | struct AboutMeshtastic: View {
11 |
12 | let locale = Locale.current
13 |
14 | var body: some View {
15 |
16 | VStack {
17 | List {
18 | Section(header: Text("What is Meshtastic?")) {
19 | Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.")
20 | .font(.title3)
21 |
22 | }
23 | Section(header: Text("Apple Apps")) {
24 |
25 | if locale.region?.identifier ?? "US" == "US" {
26 | HStack {
27 | Image("SOLAR_NODE")
28 | .resizable()
29 | .aspectRatio(contentMode: .fit)
30 | .frame(width: 75)
31 | .cornerRadius(5)
32 | .padding()
33 | VStack(alignment: .leading) {
34 | Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
35 | .font(.title2)
36 | Text("Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets.")
37 | .font(.callout)
38 | }
39 | }
40 | }
41 | Link("Help with App Development", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
42 | .font(.title2)
43 | Button("Review the app") {
44 | if let scene = UIApplication.shared.connectedScenes
45 | .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
46 | AppStore.requestReview(in: scene)
47 | }
48 | }
49 | .font(.title2)
50 |
51 | Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild))")
52 | }
53 |
54 | Section(header: Text("Project information")) {
55 | Link("Website", destination: URL(string: "https://meshtastic.org")!)
56 | .font(.title2)
57 | Link("Documentation", destination: URL(string: "https://meshtastic.org/docs/getting-started")!)
58 | .font(.title2)
59 | }
60 | Text("Meshtastic® Copyright Meshtastic LLC")
61 | .font(.caption)
62 | }
63 | }
64 | .navigationTitle("About")
65 | .navigationBarTitleDisplayMode(.inline)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Meshtastic/Views/Settings/Config/ConfigHeader.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import CoreData
3 |
4 | struct ConfigHeader: View {
5 | @EnvironmentObject var bleManager: BLEManager
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 != bleManager.connectedPeripheral?.num ?? 0 {
14 | Text("There has been no response to a request for device metadata over the admin channel for this node.")
15 | .font(.callout)
16 | .foregroundColor(.orange)
17 |
18 | } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 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 over the admin channel 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 == bleManager.connectedPeripheral?.num ?? -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/Views/Settings/Config/SaveConfigButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SaveConfigButton: View {
4 | @EnvironmentObject var bleManager: BLEManager
5 |
6 | @State private var isPresentingSaveConfirm = false
7 | let node: NodeInfoEntity?
8 | @Binding var hasChanges: Bool
9 | let onConfirmation: () -> Void
10 |
11 | var body: some View {
12 | Button {
13 | isPresentingSaveConfirm = true
14 | } label: {
15 | Label("Save", systemImage: "square.and.arrow.down")
16 | }
17 | .disabled(bleManager.connectedPeripheral == nil || !hasChanges)
18 | .buttonStyle(.bordered)
19 | .buttonBorderShape(.capsule)
20 | .controlSize(.large)
21 | .padding()
22 | .confirmationDialog(
23 | "Are you sure?",
24 | isPresented: $isPresentingSaveConfirm,
25 | titleVisibility: .visible
26 | ) {
27 | let nodeName = node?.user?.longName ?? "Unknown".localized
28 | let buttonText = String.localizedStringWithFormat("Save Config for %@".localized, nodeName)
29 | Button(buttonText) {
30 | onConfirmation()
31 | }
32 | } message: {
33 | Text("After config values save the node will reboot.")
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.19.0"),
15 | ],
16 | targets: [
17 | .target(
18 | name: "MeshtasticProtobufs",
19 | dependencies: [
20 | .product(name: "SwiftProtobuf", package: "swift-protobuf")
21 | ]
22 | )
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-black.imageset/Mesh_Logo_Black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Large.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Small.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Large.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Small.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/Widgets/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Widgets/WidgetsExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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-1080x1080.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshtastic/Meshtastic-Apple/6e5c04522631055b6e6d19eb68fd3c69394841fe/meshtastic-1080x1080.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | protoc --proto_path=./protobufs --swift_opt=Visibility=Public --swift_out=./MeshtasticProtobufs/Sources ./protobufs/meshtastic/*.proto
9 |
10 | echo "Done generating the swift files from the proto files."
11 | echo "Build, test, and commit changes."
12 |
--------------------------------------------------------------------------------
/scripts/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ./scripts/lint/lint-fix-changes.sh
--------------------------------------------------------------------------------
/scripts/lint/lint-fix-changes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Path to swiftlint
4 | SWIFT_LINT=$(which swiftlint)
5 |
6 | # Check if SwiftLint is installed
7 | if [[ -e "${SWIFT_LINT}" ]]; then
8 | count=0
9 | for file_path in $(git ls-files -m --exclude-from=.gitignore | grep ".swift$"); do
10 | export SCRIPT_INPUT_FILE_$count=$file_path
11 | count=$((count + 1))
12 | done
13 |
14 | ##### Check for modified files in unstaged/Staged area #####
15 | for file_path in $(git diff --name-only --cached | grep ".swift$"); do
16 | export SCRIPT_INPUT_FILE_$count=$file_path
17 | count=$((count + 1))
18 | done
19 |
20 | ##### Make the count available as global variable #####
21 | export SCRIPT_INPUT_FILE_COUNT=$count
22 |
23 | ##### Fix files or exit if no files found for fixing #####
24 | if [ "$count" -ne 0 ]; then
25 | echo "Found files to fix! Running swiftLint --fix..."
26 |
27 | # Run SwiftLint --fix on each file
28 | for ((i = 0; i < count; i++)); do
29 | file_var="SCRIPT_INPUT_FILE_$i"
30 | file_path=${!file_var}
31 | echo "Fixing $file_path"
32 | $SWIFT_LINT --fix "$file_path"
33 | done
34 |
35 | # Add the fixed files back to staging
36 | for ((i = 0; i < count; i++)); do
37 | file_var="SCRIPT_INPUT_FILE_$i"
38 | file_path=${!file_var}
39 | git add "$file_path"
40 | done
41 |
42 | echo "swiftLint --fix completed and files re-staged."
43 |
44 | # Optionally lint the fixed files
45 | echo "Linting fixed files..."
46 | $SWIFT_LINT lint --use-script-input-files
47 | else
48 | exit 0
49 | fi
50 |
51 | RESULT=$?
52 |
53 | if [ $RESULT -eq 0 ]; then
54 | exit 0
55 | else
56 | echo ""
57 | echo "⛔️ Violation found of the type ERROR! Please fix these issues before continuing!"
58 | fi
59 | exit $RESULT
60 |
61 | else
62 | echo "SwiftLint not installed. Please install from https://github.com/realm/SwiftLint"
63 | exit -1
64 | fi
65 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------