From f87adad009c3b7186a2e73afaf5b8d5b60952af5 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:02:24 -0700 Subject: [PATCH 1/4] Created two App Intents and made a BLEManager singleton --- Meshtastic/AppIntents/MessageChannelIntent.swift | 8 ++++++++ Meshtastic/AppIntents/SendWaypointIntent.swift | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 Meshtastic/AppIntents/MessageChannelIntent.swift create mode 100644 Meshtastic/AppIntents/SendWaypointIntent.swift diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift new file mode 100644 index 000000000..ad7bf1bca --- /dev/null +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -0,0 +1,8 @@ +// +// MessageChannelIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/9/24. +// + +import Foundation diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift new file mode 100644 index 000000000..1eb79f908 --- /dev/null +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -0,0 +1,8 @@ +// +// SendWaypointIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/9/24. +// + +import Foundation From b3f8c9299a850f7f0986a174c7441544b1087e71 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:02:55 -0700 Subject: [PATCH 2/4] Created two App Intents and made a BLEManager singleton --- Localizable.xcstrings | 33 ++++++++ Meshtastic.xcodeproj/project.pbxproj | 16 ++++ .../xcschemes/WidgetsExtension.xcscheme | 1 + .../xcshareddata/swiftpm/Package.resolved | 2 +- .../AppIntents/MessageChannelIntent.swift | 35 +++++++++ .../AppIntents/SendWaypointIntent.swift | 77 +++++++++++++++++++ Meshtastic/Helpers/BLEManager.swift | 37 +++++---- Meshtastic/MeshtasticApp.swift | 19 +++-- 8 files changed, 198 insertions(+), 22 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d80208ddc..495575ccb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2779,6 +2779,9 @@ }, "Channel Name" : { + }, + "Channel number must be between 0 and 7." : { + }, "Channel Role" : { @@ -5196,6 +5199,9 @@ }, "Description" : { + }, + "Description must be less than 100 bytes" : { + }, "Detection" : { @@ -6808,6 +6814,9 @@ } } } + }, + "Emoji" : { + }, "Empty" : { @@ -7176,6 +7185,9 @@ }, "Factory reset your device and app? " : { + }, + "Failed to encode message content." : { + }, "Failed to get a valid position to exchange" : { @@ -10855,6 +10867,9 @@ }, "Loading Logs. . ." : { + }, + "Location" : { + }, "Location: %@" : { @@ -14500,6 +14515,12 @@ } } } + }, + "Message" : { + + }, + "Message content exceeds 228 bytes." : { + }, "message.details" : { "localizations" : { @@ -15103,6 +15124,9 @@ } } } + }, + "Must be a single emoji" : { + }, "Nag timeout" : { @@ -15167,6 +15191,9 @@ }, "Name" : { + }, + "Name must be less than 30 bytes" : { + }, "Nearby Topics" : { @@ -18995,6 +19022,12 @@ }, "Send" : { + }, + "Send a channel message" : { + + }, + "Send a waypoint" : { + }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cca267e76..0bf7d5369 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; + BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; + BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -262,6 +264,8 @@ 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; + BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; + BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -548,6 +552,15 @@ path = MeshtasticTests; sourceTree = ""; }; + BCB6137F2C6728E700485544 /* AppIntents */ = { + isa = PBXGroup; + children = ( + BCB613802C67290800485544 /* SendWaypointIntent.swift */, + BCB613822C672A2600485544 /* MessageChannelIntent.swift */, + ); + path = AppIntents; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -814,6 +827,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + BCB6137F2C6728E700485544 /* AppIntents */, DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, DD7709392AA1ABA1007A8BF0 /* Tips */, @@ -1339,6 +1353,7 @@ DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD3CC24C2C498D6C001BD3A2 /* BatteryCompact.swift in Sources */, + BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */, DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, @@ -1375,6 +1390,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, + BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme index 880339bcd..decd8381b 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme @@ -89,6 +89,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index e358787d2..c47a90583 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "af29d93455cb8f728684674f544d815b5becb17e049287cc1df8079a4855d0fc", + "originHash" : "1571e0d09fede5d57a2c415019f30868d90fde5a53a863cc277593881c2dc4a5", "pins" : [ { "identity" : "cocoamqtt", diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index ad7bf1bca..a952e5ff8 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -6,3 +6,38 @@ // import Foundation +import AppIntents + +struct MessageChannelIntent: AppIntent { + static var title: LocalizedStringResource = "Send a channel message" + + @Parameter(title: "Message") + var messageContent: String + + @Parameter(title: "Channel",controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7)) + var channelNumber: Int + + + static var parameterSummary: some ParameterSummary { + Summary("Send \(\.$messageContent) to \(\.$channelNumber)") + } + func perform() async throws -> some IntentResult { + // Check if channel number is between 1 and 7 + guard (0...7).contains(channelNumber) else { + throw $channelNumber.needsValueError("Channel number must be between 0 and 7.") + } + + // Convert messageContent to data and check its length + guard let messageData = messageContent.data(using: .utf8) else { + throw $messageContent.needsValueError("Failed to encode message content.") + } + + if messageData.count > 228 { + throw $messageContent.needsValueError("Message content exceeds 228 bytes.") + } + if (BLEManager.shared.isConnected){ + BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) + } + return .result() + } +} diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 1eb79f908..43cc93c8f 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -5,4 +5,81 @@ // Created by Benjamin Faershtein on 8/9/24. // +import CoreLocation import Foundation +import AppIntents +import MeshtasticProtobufs + +struct SendWaypointIntent: AppIntent { + + static var title = LocalizedStringResource("Send a waypoint") + + @Parameter(title: "Name", default: "Dropped Pin") + var nameParameter: String? + + @Parameter(title: "Description", default: "") + var descriptionParameter: String? + + @Parameter(title: "Emoji", default: "📍") + var emojiParameter: String? + + @Parameter(title: "Location") + var locationParameter: CLPlacemark + + func perform() async throws -> some IntentResult { + + // Provide default values if parameters are nil + let name = nameParameter ?? "Dropped Pin" + let description = descriptionParameter ?? "" + let emoji = emojiParameter ?? "📍" + + // Validate name length + if name.utf8.count > 30 { + throw $nameParameter.needsValueError("Name must be less than 30 bytes") + } + + // Validate description length + if description.utf8.count > 100 { + throw $descriptionParameter.needsValueError("Description must be less than 100 bytes") + } + + // Validate emoji + guard isValidSingleEmoji(emoji) else { + throw $emojiParameter.needsValueError("Must be a single emoji") + } + + var newWaypoint = Waypoint() + + if let latitude = locationParameter.location?.coordinate.latitude { + newWaypoint.latitudeI = Int32(latitude * 10_000_000) + } + + if let longitude = locationParameter.location?.coordinate.longitude { + newWaypoint.longitudeI = Int32(longitude * 10_000_000) + } + + + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + // This regex pattern is for matching a single emoji + let emojiPattern = "^([\\p{So}\\p{Cn}])$" + let regex = try? NSRegularExpression(pattern: emojiPattern, options: []) + let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count)) + + return matches?.count == 1 + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 475235de8..4db1197cf 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -11,6 +11,7 @@ import OSLog // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject { + static var shared: BLEManager! // Singleton instance let appState: AppState @@ -54,20 +55,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MARK: init - init( - appState: AppState, - context: NSManagedObjectContext - ) { - self.appState = appState - self.context = context + private override init() { + // Default initialization should not be used + fatalError("Use setup(appState:context:) to initialize the singleton") + } + + static func setup(appState: AppState, context: NSManagedObjectContext) { + guard shared == nil else { + print("BLEManager already initialized") + return + } + shared = BLEManager(appState: appState, context: context) + } + + private init(appState: AppState, context: NSManagedObjectContext) { + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + } + - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self - // centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey]) - } // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 4d7dc4c16..c8ba6a05d 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -17,8 +17,8 @@ struct MeshtasticAppleApp: App { @ObservedObject var appState: AppState - @ObservedObject - private var bleManager: BLEManager +// @ObservedObject +// private var bleManager: BLEManager private let persistenceController: PersistenceController @@ -35,10 +35,13 @@ struct MeshtasticAppleApp: App { ) self._appState = ObservedObject(wrappedValue: appState) - self.bleManager = BLEManager( - appState: appState, - context: persistenceController.container.viewContext - ) +// self.bleManager = BLEManager( +// appState: appState, +// context: persistenceController.container.viewContext +// ) + + // Initialize the BLEManager singleton with the necessary dependencies + BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController // Wire up router @@ -53,9 +56,9 @@ struct MeshtasticAppleApp: App { ) .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(appState) - .environmentObject(bleManager) + .environmentObject(BLEManager.shared) .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager) + SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: BLEManager.shared) .presentationDetents([.large]) .presentationDragIndicator(.visible) } From 0d40e5de5dda761f6d57f78bd21d4e7f867cfbd4 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:05:23 -0700 Subject: [PATCH 3/4] Cleanup --- Meshtastic/MeshtasticApp.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index c8ba6a05d..f926956c1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -35,11 +35,6 @@ struct MeshtasticAppleApp: App { ) self._appState = ObservedObject(wrappedValue: appState) -// self.bleManager = BLEManager( -// appState: appState, -// context: persistenceController.container.viewContext -// ) - // Initialize the BLEManager singleton with the necessary dependencies BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController From 3e1c5b61190f628d37ca29c768c5b79dc7f53875 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:13:45 -0700 Subject: [PATCH 4/4] Created Error handling and a new Node Position Intent --- Localizable.xcstrings | 23 +++++++- Meshtastic.xcodeproj/project.pbxproj | 8 +++ Meshtastic/AppIntents/AppIntentErrors.swift | 22 ++++++++ .../AppIntents/MessageChannelIntent.swift | 14 ++++- .../AppIntents/NodePositionIntent.swift | 56 +++++++++++++++++++ .../AppIntents/SendWaypointIntent.swift | 10 +++- 6 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 Meshtastic/AppIntents/AppIntentErrors.swift create mode 100644 Meshtastic/AppIntents/NodePositionIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 495575ccb..a3bd11339 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4956,6 +4956,9 @@ } } } + }, + "Could not find node" : { + }, "Counter Clockwise Rotary Event" : { @@ -6984,6 +6987,9 @@ }, "Erase all device and app data?" : { + }, + "Error: %@" : { + }, "ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message." : { @@ -7186,7 +7192,7 @@ "Factory reset your device and app? " : { }, - "Failed to encode message content." : { + "Failed to encode message content" : { }, "Failed to get a valid position to exchange" : { @@ -7200,6 +7206,9 @@ }, "Favorites" : { + }, + "Fetch the latest position of a cetain node" : { + }, "Fifteen Minutes" : { @@ -7614,6 +7623,9 @@ }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { + }, + "Get Node Position" : { + }, "Get NRF DFU from the App Store" : { @@ -15325,6 +15337,9 @@ }, "Newer firmware is available" : { + }, + "No Connected Node" : { + }, "No Device Metrics" : { @@ -15406,6 +15421,9 @@ } } } + }, + "Node does not have positions" : { + }, "Node History" : { @@ -19025,6 +19043,9 @@ }, "Send a channel message" : { + }, + "Send a message to a certain meshtastic channel" : { + }, "Send a waypoint" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0bf7d5369..49a91c7dd 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; + BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; + BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -266,6 +268,8 @@ B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; + BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; + BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -557,6 +561,8 @@ children = ( BCB613802C67290800485544 /* SendWaypointIntent.swift */, BCB613822C672A2600485544 /* MessageChannelIntent.swift */, + BCB613842C68703800485544 /* NodePositionIntent.swift */, + BCB613862C69A0FB00485544 /* AppIntentErrors.swift */, ); path = AppIntents; sourceTree = ""; @@ -1368,6 +1374,7 @@ DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, + BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */, DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, @@ -1384,6 +1391,7 @@ DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, + BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, diff --git a/Meshtastic/AppIntents/AppIntentErrors.swift b/Meshtastic/AppIntents/AppIntentErrors.swift new file mode 100644 index 000000000..c20ead7ca --- /dev/null +++ b/Meshtastic/AppIntents/AppIntentErrors.swift @@ -0,0 +1,22 @@ +// +// AppIntentErrors.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/11/24. +// + +import Foundation + +class AppIntentErrors { + enum AppIntentError: Swift.Error, CustomLocalizedStringResourceConvertible { + case notConnected + case message(_ message: String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case let .message(message): return "Error: \(message)" + case .notConnected: return "No Connected Node" + } + } + } +} diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index a952e5ff8..53e202a67 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -11,6 +11,8 @@ import AppIntents struct MessageChannelIntent: AppIntent { static var title: LocalizedStringResource = "Send a channel message" + static var description: IntentDescription = "Send a message to a certain meshtastic channel" + @Parameter(title: "Message") var messageContent: String @@ -22,6 +24,10 @@ struct MessageChannelIntent: AppIntent { Summary("Send \(\.$messageContent) to \(\.$channelNumber)") } func perform() async throws -> some IntentResult { + if (!BLEManager.shared.isConnected){ + throw AppIntentErrors.AppIntentError.notConnected + } + // Check if channel number is between 1 and 7 guard (0...7).contains(channelNumber) else { throw $channelNumber.needsValueError("Channel number must be between 0 and 7.") @@ -29,15 +35,17 @@ struct MessageChannelIntent: AppIntent { // Convert messageContent to data and check its length guard let messageData = messageContent.data(using: .utf8) else { - throw $messageContent.needsValueError("Failed to encode message content.") + throw AppIntentErrors.AppIntentError.message("Failed to encode message content") } if messageData.count > 228 { throw $messageContent.needsValueError("Message content exceeds 228 bytes.") } - if (BLEManager.shared.isConnected){ - BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) + + if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){ + throw AppIntentErrors.AppIntentError.message("Failed to send message") } + return .result() } } diff --git a/Meshtastic/AppIntents/NodePositionIntent.swift b/Meshtastic/AppIntents/NodePositionIntent.swift new file mode 100644 index 000000000..496ca3e39 --- /dev/null +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -0,0 +1,56 @@ +// +// NodePositionIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/10/24. +// + +import Foundation +import AppIntents +import CoreLocation +import CoreData + +struct NodePositionIntent: AppIntent { + + @Parameter(title: "Node Number") + var nodeNum: Int + + static var title: LocalizedStringResource = "Get Node Position" + static var description: IntentDescription = "Fetch the latest position of a cetain node" + + + func perform() async throws -> some IntentResult & ReturnsValue { + if (!BLEManager.shared.isConnected) { + throw AppIntentErrors.AppIntentError.notConnected + } + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + do { + guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else { + throw $nodeNum.needsValueError("Could not find node") + } + + let nodeInfo = fetchedNode[0] + if let latitude = nodeInfo.latestPosition?.coordinate.latitude, + let longitude = nodeInfo.latestPosition?.coordinate.longitude { + let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) + + // Reverse geocode the CLLocation to get a CLPlacemark + let geocoder = CLGeocoder() + let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation) + + if let placemark = placemarks.first { + return .result(value: placemark) + } else { + throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location") + } + } else { + throw AppIntentErrors.AppIntentError.message("Node does not have positions") + } + } catch { + throw AppIntentErrors.AppIntentError.message("Fetch Failure") + } + } + +} + diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 43cc93c8f..a94e8b7d2 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -27,7 +27,9 @@ struct SendWaypointIntent: AppIntent { var locationParameter: CLPlacemark func perform() async throws -> some IntentResult { - + if (!BLEManager.shared.isConnected){ + throw AppIntentErrors.AppIntentError.notConnected + } // Provide default values if parameters are nil let name = nameParameter ?? "Dropped Pin" let description = descriptionParameter ?? "" @@ -67,10 +69,12 @@ struct SendWaypointIntent: AppIntent { newWaypoint.icon = unicode newWaypoint.name = name newWaypoint.description_p = description - if (BLEManager.shared.isConnected){ - BLEManager.shared.sendWaypoint(waypoint: newWaypoint) + if(!BLEManager.shared.sendWaypoint(waypoint: newWaypoint)){ + throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint") } + + return .result() }