├── .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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mesh_Logo_White.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/logo-white.imageset/Mesh_Logo_White.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "progress.ring.dashed.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-black.imageset/Mesh_Logo_Black_Small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/m-logo-white.imageset/Mesh_Logo_White_Small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------