diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 05c2bbb8..a61ea1cf 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21,21 +21,6 @@ }, ": %d" : { - }, - ".dot" : { - - }, - ".gauge" : { - - }, - ".gradient" : { - - }, - ".pill" : { - - }, - ".text" : { - }, "(Re)define PIN_GPS_EN for your board." : { @@ -1429,8 +1414,15 @@ "Bad" : { }, - "Bad Packets: %d" : { - + "Bad Packets: %d %@%%" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bad Packets: %1$d %2$@%%" + } + } + } }, "Bandwidth" : { @@ -5061,9 +5053,6 @@ }, "Debug" : { - }, - "Debug Log" : { - }, "Debug Logs" : { @@ -15907,6 +15896,9 @@ }, "OK" : { + }, + "Ok to MQTT" : { + }, "OLED Type" : { @@ -16079,11 +16071,15 @@ "Override automatic OLED screen detection." : { }, - "Packets Received: %d" : { - - }, - "Packets Sent: %d" : { - + "Packets: Sent: %d Received: %d" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Packets: Sent: %1$d Received: %2$d" + } + } + } }, "password" : { "localizations" : { @@ -19158,6 +19154,9 @@ }, "Send" : { + }, + "Send ${messageContent} to ${channelNumber}" : { + }, "Send a Group Message" : { @@ -22314,6 +22313,9 @@ } } } + }, + "Uptime: %@" : { + }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 43f27393..7d9209f1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -376,6 +376,7 @@ DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = ""; }; + DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 44.xcdatamodel"; sourceTree = ""; }; DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; @@ -1686,7 +1687,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1721,7 +1722,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1753,7 +1754,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1786,7 +1787,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1898,6 +1899,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, @@ -1942,7 +1944,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */; + currentVersion = DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index c47a9058..3465793d 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "d57a5aecf24a25b32ec4a74be2f5d0a995a47c4b", - "version" : "1.27.0" + "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", + "version" : "1.28.1" } } ], diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json index 937092d6..241cfb2b 100644 --- a/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -5,6 +5,30 @@ "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "logo-tinted.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png new file mode 100644 index 00000000..9942a312 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png new file mode 100644 index 00000000..85915643 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png differ diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 87551c4f..aae1a8ab 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -3302,7 +3302,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { @@ -3312,9 +3312,9 @@ extension BLEManager: CBCentralManagerDelegate { } else { isSwitchedOn = false } - + var status = "" - + switch central.state { case .poweredOff: status = "BLE is powered off" @@ -3333,10 +3333,10 @@ extension BLEManager: CBCentralManagerDelegate { } Logger.services.info("📜 [BLE] Bluetooth status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - + if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)") @@ -3344,7 +3344,7 @@ extension BLEManager: CBCentralManagerDelegate { let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - + if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 3e41c8f3..1260f20c 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -5,6 +5,16 @@ import OSLog class LocalNotificationManager { var notifications = [Notification]() + + let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: + "👍 \(Tapbacks.thumbsUp.description)", options: []) + let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: + "👎 \(Tapbacks.thumbsDown.description)", options: []) + let replyInputAction = UNTextInputNotificationAction( + identifier: "messageNotification.replyInputAction", + title: "reply".localized, + options: []) + // Step 1 Request Permissions for notifications private func requestAuthorization() { @@ -31,6 +41,15 @@ class LocalNotificationManager { // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future private func scheduleNotifications() { + let messageNotificationCategory = UNNotificationCategory( + identifier: "messageNotificationCategory", + actions: [thumbsUpAction, thumbsDownAction,replyInputAction], + intentIdentifiers: [], + options: .customDismissAction + ) + + UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) + for notification in notifications { let content = UNMutableNotificationContent() content.subtitle = notification.subtitle @@ -45,6 +64,17 @@ class LocalNotificationManager { if notification.path != nil { content.userInfo["path"] = notification.path } + if notification.messageId != nil { + content.categoryIdentifier = "messageNotificationCategory" + content.userInfo["messageId"] = notification.messageId + } + if notification.channel != nil { + content.userInfo["channel"] = notification.channel + } + if notification.userNum != nil { + content.userInfo["userNum"] = notification.userNum + } + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) @@ -76,4 +106,7 @@ struct Notification { var content: String var target: String? var path: String? + var messageId: Int64? + var channel: Int32? + var userNum: Int64? } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3b6b2b65..09683c0e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -816,12 +816,10 @@ func textMessageAppPacket( context: NSManagedObjectContext, appState: AppState ) { - var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) let rangeRef = Reference(Int.self) let rangeTestRegex = Regex { "seq " - TryCapture(as: rangeRef) { OneOrMore(.digit) } transform: { match in @@ -829,7 +827,7 @@ func textMessageAppPacket( } } let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false - + if !wantRangeTestPackets && rangeTest { return } @@ -842,15 +840,16 @@ func textMessageAppPacket( } } } - + if messageText?.count ?? 0 > 0 { - MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") - + let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) + do { let fetchedUsers = try context.fetch(messageUsers) + let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) if packet.rxTime > 0 { @@ -864,56 +863,66 @@ func textMessageAppPacket( newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) - newMessage.publicKey = packet.publicKey - newMessage.pkiEncrypted = packet.pkiEncrypted + if newMessage.toUser?.pkiEncrypted ?? false { + newMessage.pkiEncrypted = true + newMessage.publicKey = packet.publicKey + } if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true } } + if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - + if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { if !storeForwardBroadcast { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } } + if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) { - /// We have a key, check if it matches + + if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) && newMessage.toUser != nil && packet.pkiEncrypted { + // We have a key and it is a PKC encrypted DM, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey + Logger.data.error("🔑 Key Mismatch origninal key: \(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key") new key: \(newMessage.fromUser?.newPublicKey?.base64EncodedString() ?? "No Key") ") } } else { - /// We have no key, set it - newMessage.fromUser?.publicKey = packet.publicKey - newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted + /// We have no key, set it if it is not empty + if !packet.publicKey.isEmpty { + newMessage.fromUser?.pkiEncrypted = true + newMessage.fromUser?.publicKey = packet.publicKey + } } + if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } else { newMessage.fromUser?.userNode?.lastHeard = Date() } } + newMessage.messagePayload = messageText newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) + if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { newMessage.fromUser?.lastMessage = Date() } + var messageSaved = false - + do { - try context.save() Logger.data.info("💾 Saved a new message for \(newMessage.messageId)") messageSaved = true if messageSaved { - if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { return } @@ -932,14 +941,16 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)" + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(packet.from) ) ] manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") } } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) @@ -962,7 +973,11 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)") + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0") + ) ] manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") @@ -970,7 +985,7 @@ func textMessageAppPacket( } } } catch { - + // Handle error } } } @@ -985,6 +1000,7 @@ func textMessageAppPacket( } } + func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 04d3cc1a..8a88003b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 43.xcdatamodel + MeshtasticDataModelV 44.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents new file mode 100644 index 00000000..98de0347 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 06d290e9..d38557bc 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -9,9 +9,9 @@ import SwiftUI import OSLog class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { - + var router: Router? - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Logger.services.info("🚀 [App] Meshtstic Apple App launched!") // Default User Default Values @@ -22,7 +22,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if #available(iOS 17.0, macOS 14.0, *) { let locationsHandler = LocationsHandler.shared locationsHandler.startLocationUpdates() - + // If a background activity session was previously active, reinstantiate it after the background launch. if locationsHandler.backgroundActivity { locationsHandler.backgroundActivity = true @@ -38,7 +38,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat ) { completionHandler([.list, .banner, .sound]) } - + // This method is called when a user clicks on the notification func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -46,6 +46,64 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo + + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + break + + case "messageNotification.thumbsUpAction": + if let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: Tapbacks.thumbsUp.emojiString, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: true, + replyID: replyID + ) + Logger.services.info("Tapback response sent") + } else { + Logger.services.error("Failed to retrieve channel or messageId from userInfo") + } + break + + case "messageNotification.thumbsDownAction": + if let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: Tapbacks.thumbsDown.emojiString, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: true, + replyID: replyID + ) + Logger.services.info("Tapback response sent") + } else { + Logger.services.error("Failed to retrieve channel or messageId from userInfo") + } + break + + case "messageNotification.replyInputAction": + if let userInput = (response as? UNTextInputNotificationResponse)?.userText, + let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: userInput, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: false, + replyID: replyID + ) + Logger.services.info("Actionable notification reply sent") + } else { + Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo") + } + break + + default: + break + } + if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { @@ -54,7 +112,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to handle notification response: \(userInfo)") } - + completionHandler() } } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 624dbb20..faa8e606 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -181,8 +181,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) - newUser.pkiEncrypted = packet.pkiEncrypted - newUser.publicKey = packet.publicKey + if !newUserMessage.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = newUserMessage.publicKey + } + Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) @@ -209,8 +212,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { if packet.from > Constants.minimumNodeNum { let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user?.pkiEncrypted = packet.pkiEncrypted - newNode.user?.publicKey = packet.publicKey + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } newNode.user = newUser } } @@ -224,6 +229,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) myInfoEntity.rebootCount = 0 do { try context.save() + Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)") Logger.data.info("💾 [MyInfoEntity] Saved a new myInfo for node number: \(packet.from.toHex(), privacy: .public)") } catch { context.rollback() @@ -271,10 +277,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) - - if !packet.publicKey.isEmpty { - fetchedNode[0].user!.pkiEncrypted = packet.pkiEncrypted - fetchedNode[0].user!.publicKey = packet.publicKey + if !nodeInfoMessage.user.publicKey.isEmpty { + fetchedNode[0].user!.pkiEncrypted = true + fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey } Task { Api().loadDeviceHardwareData { (hw) in @@ -458,7 +463,6 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi let newDeviceConfig = DeviceConfigEntity(context: context) newDeviceConfig.role = Int32(config.role.rawValue) newDeviceConfig.serialEnabled = config.serialEnabled - newDeviceConfig.debugLogEnabled = config.debugLogEnabled newDeviceConfig.buttonGpio = Int32(config.buttonGpio) newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) @@ -471,7 +475,6 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi } else { fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) fetchedNode[0].deviceConfig?.serialEnabled = config.serialEnabled - fetchedNode[0].deviceConfig?.debugLogEnabled = config.debugLogEnabled fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) @@ -595,6 +598,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa newLoRaConfig.channelNum = Int32(config.channelNum) newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain newLoRaConfig.ignoreMqtt = config.ignoreMqtt + newLoRaConfig.okToMqtt = config.configOkToMqtt fetchedNode[0].loRaConfig = newLoRaConfig } else { fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) @@ -612,6 +616,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt + fetchedNode[0].loRaConfig?.okToMqtt = config.configOkToMqtt fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } if sessionPasskey != nil { @@ -813,21 +818,23 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s let newSecurityConfig = SecurityConfigEntity(context: context) newSecurityConfig.publicKey = config.publicKey newSecurityConfig.privateKey = config.privateKey - newSecurityConfig.adminKey = config.adminKey + if config.adminKey.count > 0 { + newSecurityConfig.adminKey = config.adminKey[0] + } newSecurityConfig.isManaged = config.isManaged newSecurityConfig.serialEnabled = config.serialEnabled newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled - newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled fetchedNode[0].securityConfig = newSecurityConfig } else { fetchedNode[0].securityConfig?.publicKey = config.publicKey fetchedNode[0].securityConfig?.privateKey = config.privateKey - fetchedNode[0].securityConfig?.adminKey = config.adminKey + if config.adminKey.count > 0 { + fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] + } fetchedNode[0].securityConfig?.isManaged = config.isManaged fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled - fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled } if sessionPasskey?.count != 0 { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 2a1eea84..3fdab7fd 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -92,6 +92,7 @@ struct Connect: View { } VStack { let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity + if localStats != nil { Divider() if localStats?.numTotalNodes ?? 0 >= 100 { @@ -111,25 +112,29 @@ struct Connect: View { .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) - Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)") + Text("Packets: Sent: \(localStats?.numPacketsTx ?? 0) Received: \(localStats?.numPacketsRx ?? 0)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) - Text("Packets Received: \(localStats?.numPacketsRx ?? 0)") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") + let errorRate = (Double(localStats?.numPacketsRxBad ?? -1) / Double(localStats?.numPacketsRx ?? -1)) * 100 + Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0) \(String(format: "Error Rate: %.2f", errorRate))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .fixedSize() + let now = Date.now + let later = now + TimeInterval(Double(localStats?.numPacketsRxBad ?? 0)) + let uptime = (now.. Color { } func getLoRaSignalStrength(snr: Float, rssi: Int32, preset: ModemPresets) -> LoRaSignalStrength { + // rssi is 0 when not available + if rssi == 0 { + if snr > (preset.snrLimit()) { + return .good + } + if snr < (preset.snrLimit() - 7.5) { + return .none + } + if snr <= (preset.snrLimit() - 5.5) { + return .bad + } + return .fair + } + if rssi > -115 && snr > (preset.snrLimit()) { return .good } else if rssi < -126 && snr < (preset.snrLimit() - 7.5) { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4e09859f..4db57180 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -35,7 +35,6 @@ struct UserList: View { var boolFilters: [Bool] {[ isFavorite, isOnline, - isPkiEncrypted, isEnvironment, distanceFilter, roleFilter @@ -45,11 +44,12 @@ struct UserList: View { sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), NSSortDescriptor(key: "pkiEncrypted", ascending: false), + NSSortDescriptor(key: "userNode.lastHeard", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], - predicate: NSPredicate(format: "hwModelId != nil"), + predicate: NSPredicate(format: "longName != ''"), animation: .default ) - private var users: FetchedResults + var users: FetchedResults @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -202,34 +202,55 @@ struct UserList: View { DirectMessagesHelp() } .onChange(of: searchText) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: viaLora) { _ in if !viaLora && !viaMqtt { viaMqtt = true } - searchUserList() + Task { + await searchUserList() + } } .onChange(of: viaMqtt) { _ in if !viaLora && !viaMqtt { viaLora = true } - searchUserList() + Task { + await searchUserList() + } } .onChange(of: [deviceRoles]) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: hopsAway) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: [boolFilters]) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: maxDistance) { _ in - searchUserList() + Task { + await searchUserList() + } + } + .onChange(of: isPkiEncrypted) { _ in + Task { + await searchUserList() + } } .onAppear { - searchUserList() + Task { + await searchUserList() + } } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { @@ -267,8 +288,7 @@ struct UserList: View { .scrollDismissesKeyboard(.immediately) } } - - private func searchUserList() { + private func searchUserList() async { /// Case Insensitive Search Text Predicates let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 7906f8b5..340117ac 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -130,7 +130,9 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) + if let mrCoord = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: mrCoord, distance: 8000, heading: 0, pitch: 60)) + } } if self.scene == nil { Task { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index f039534c..ca55b95d 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -93,7 +93,7 @@ struct Channels: View { preciseLocation = true positionsEnabled = true if channelKey == "AQ==" { - positionPrecision = 13 + positionPrecision = 14 preciseLocation = false } } else if !supportedVersion && channelRole == 2 { @@ -103,8 +103,8 @@ struct Channels: View { } else { if channelKey == "AQ==" { preciseLocation = false - if (positionPrecision > 0 && positionPrecision < 10) || positionPrecision > 16 { - positionPrecision = 13 + if (positionPrecision > 0 && positionPrecision < 11) || positionPrecision > 14 { + positionPrecision = 14 } } else if positionPrecision == 32 { preciseLocation = true diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index d21e0f42..e4930b7a 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -149,7 +149,7 @@ struct ChannelForm: View { .listRowSeparator(.visible) .onChange(of: preciseLocation) { pl in if pl == false { - positionPrecision = 13 + positionPrecision = 14 } } } @@ -158,7 +158,7 @@ struct ChannelForm: View { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 10...16, step: 1) { + Slider(value: $positionPrecision, in: 11...14, step: 1) { } minimumValueLabel: { Image(systemName: "minus") } maximumValueLabel: { @@ -220,7 +220,7 @@ struct ChannelForm: View { } positionPrecision = 32 } else { - positionPrecision = 13 + positionPrecision = 14 } hasChanges = true } @@ -230,7 +230,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { pe in if pe { if positionPrecision == 0 { - positionPrecision = 13 + positionPrecision = 14 } } else { positionPrecision = 0 diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index dc17446e..c78cb845 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -24,7 +24,6 @@ struct DeviceConfig: View { @State var buzzerGPIO = 0 @State var buttonGPIO = 0 @State var serialEnabled = true - @State var debugLogEnabled = false @State var rebroadcastMode = 0 @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @@ -89,10 +88,6 @@ struct DeviceConfig: View { Label("Serial Console", systemImage: "terminal") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $debugLogEnabled) { - Label("Debug Log", systemImage: "ant.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack(alignment: .leading) { HStack { Label("Time Zone", systemImage: "clock.badge.exclamationmark") @@ -200,7 +195,6 @@ struct DeviceConfig: View { var dc = Config.DeviceConfig() dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue() dc.serialEnabled = serialEnabled - dc.debugLogEnabled = debugLogEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() @@ -259,9 +253,6 @@ struct DeviceConfig: View { .onChange(of: serialEnabled) { if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true } } - .onChange(of: debugLogEnabled) { - if $0 != node?.deviceConfig?.debugLogEnabled { hasChanges = true } - } .onChange(of: buttonGPIO) { newButtonGPIO in if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } @@ -289,7 +280,6 @@ struct DeviceConfig: View { func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) self.serialEnabled = (node?.deviceConfig?.serialEnabled ?? true) - self.debugLogEnabled = node?.deviceConfig?.debugLogEnabled ?? false self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0) self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 43729f86..13623ee3 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -45,6 +45,7 @@ struct LoRaConfig: View { @State var rxBoostedGain = false @State var overrideFrequency: Float = 0.0 @State var ignoreMqtt = false + @State var okToMqtt = false let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -100,6 +101,10 @@ struct LoRaConfig: View { Label("Ignore MQTT", systemImage: "server.rack") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $okToMqtt) { + Label("Ok to MQTT", systemImage: "network") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $txEnabled) { Label("Transmit Enabled", systemImage: "waveform.path") @@ -209,6 +214,7 @@ struct LoRaConfig: View { lc.sx126XRxBoostedGain = rxBoostedGain lc.overrideFrequency = overrideFrequency lc.ignoreMqtt = ignoreMqtt + lc.configOkToMqtt = okToMqtt if connectedNode?.num ?? -1 == node?.user?.num ?? 0 { UserDefaults.modemPreset = modemPreset } @@ -292,6 +298,9 @@ struct LoRaConfig: View { .onChange(of: ignoreMqtt) { if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true } } + .onChange(of: okToMqtt) { + if $0 != node?.loRaConfig?.okToMqtt { hasChanges = true } + } } func setLoRaValues() { self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) @@ -307,6 +316,7 @@ struct LoRaConfig: View { self.rxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0 self.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false + self.okToMqtt = node?.loRaConfig?.okToMqtt ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index d6cbbf2b..1996c744 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -32,8 +32,7 @@ struct MQTTConfig: View { @State var nearbyTopics = [String]() @State var mapReportingEnabled = false @State var mapPublishIntervalSecs = 3600 - @State var preciseLocation: Bool = false - @State var mapPositionPrecision: Double = 13.0 + @State var mapPositionPrecision: Double = 14.0 let locale = Locale.current @@ -105,35 +104,17 @@ struct MQTTConfig: View { } } .pickerStyle(DefaultPickerStyle()) - VStack(alignment: .leading) { - Toggle(isOn: $preciseLocation) { - Label("Precise Location", systemImage: "scope") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - .onChange(of: preciseLocation) { pl in - if pl == false { - mapPositionPrecision = 12 - } else { - mapPositionPrecision = 32 - } - } - } - - if !preciseLocation { - VStack(alignment: .leading) { - Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $mapPositionPrecision, in: 11...16, step: 1) { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") - } - Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") - .foregroundColor(.gray) - .font(.callout) + Label("Approximate Location", systemImage: "location.slash.circle.fill") + Slider(value: $mapPositionPrecision, in: 11...14, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") } + Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") + .foregroundColor(.gray) + .font(.callout) } } } @@ -429,11 +410,12 @@ struct MQTTConfig: View { self.mqttConnected = bleManager.mqttProxyConnected self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) - self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 12) - if mapPositionPrecision == 0.0 { - self.mapPositionPrecision = 12 + self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14) + if mapPositionPrecision < 11 || mapPositionPrecision > 14 { + self.mapPositionPrecision = 14 + self.hasChanges = true + } else { + self.hasChanges = false } - self.preciseLocation = mapPositionPrecision == 32 - self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e58de058..95a07dfb 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -186,7 +186,7 @@ struct SecurityConfig: View { var config = Config.SecurityConfig() config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() - config.adminKey = Data(base64Encoded: adminKey) ?? Data() + config.adminKey = [Data(base64Encoded: adminKey) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 0f1c3586..53d244d1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -844,6 +844,7 @@ public struct AdminMessage { /// /// TODO: REPLACE case securityConfig // = 7 + case sessionkeyConfig // = 8 case UNRECOGNIZED(Int) public init() { @@ -860,6 +861,7 @@ public struct AdminMessage { case 5: self = .loraConfig case 6: self = .bluetoothConfig case 7: self = .securityConfig + case 8: self = .sessionkeyConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -874,6 +876,7 @@ public struct AdminMessage { case .loraConfig: return 5 case .bluetoothConfig: return 6 case .securityConfig: return 7 + case .sessionkeyConfig: return 8 case .UNRECOGNIZED(let i): return i } } @@ -998,6 +1001,7 @@ extension AdminMessage.ConfigType: CaseIterable { .loraConfig, .bluetoothConfig, .securityConfig, + .sessionkeyConfig, ] } @@ -1755,6 +1759,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "LORA_CONFIG"), 6: .same(proto: "BLUETOOTH_CONFIG"), 7: .same(proto: "SECURITY_CONFIG"), + 8: .same(proto: "SESSIONKEY_CONFIG"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 4b953470..37832baa 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -93,6 +93,14 @@ public struct Config { set {payloadVariant = .security(newValue)} } + public var sessionkey: Config.SessionkeyConfig { + get { + if case .sessionkey(let v)? = payloadVariant {return v} + return Config.SessionkeyConfig() + } + set {payloadVariant = .sessionkey(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -106,6 +114,7 @@ public struct Config { case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) + case sessionkey(Config.SessionkeyConfig) #if !swift(>=4.1) public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { @@ -145,6 +154,10 @@ public struct Config { guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } return l == r }() + case (.sessionkey, .sessionkey): return { + guard case .sessionkey(let l) = lhs, case .sessionkey(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -167,12 +180,6 @@ public struct Config { /// Moved to SecurityConfig public var serialEnabled: Bool = false - /// - /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Set this to true to leave the debug log outputting even when API is active. - /// Moved to SecurityConfig - public var debugLogEnabled: Bool = false - /// /// For boards without a hard wired button, this is the pin number that will be used /// Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined. @@ -1250,6 +1257,13 @@ public struct Config { set {_uniqueStorage()._ignoreMqtt = newValue} } + /// + /// Sets the ok_to_mqtt bit on outgoing packets + public var configOkToMqtt: Bool { + get {return _storage._configOkToMqtt} + set {_uniqueStorage()._configOkToMqtt = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum RegionCode: SwiftProtobuf.Enum { @@ -1492,11 +1506,6 @@ public struct Config { /// Specified PIN for PairingMode.FixedPin public var fixedPin: UInt32 = 0 - /// - /// Enables device (serial style logs) over Bluetooth - /// Moved to SecurityConfig - public var deviceLoggingEnabled: Bool = false - public var unknownFields = SwiftProtobuf.UnknownStorage() public enum PairingMode: SwiftProtobuf.Enum { @@ -1559,7 +1568,7 @@ public struct Config { /// /// The public key authorized to send admin messages to this node. - public var adminKey: Data = Data() + public var adminKey: [Data] = [] /// /// If true, device is considered to be "managed" by a mesh administrator via admin messages @@ -1572,13 +1581,9 @@ public struct Config { /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Output live debug logging over serial. + /// Output live debug logging over serial or bluetooth is set to true. public var debugLogApiEnabled: Bool = false - /// - /// Enables device (serial style logs) over Bluetooth - public var bluetoothLoggingEnabled: Bool = false - /// /// Allow incoming device control over the insecure legacy admin channel. public var adminChannelEnabled: Bool = false @@ -1588,6 +1593,18 @@ public struct Config { public init() {} } + /// + /// Blank config request, strictly for getting the session key + public struct SessionkeyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1784,6 +1801,7 @@ extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} extension Config.BluetoothConfig: @unchecked Sendable {} extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} extension Config.SecurityConfig: @unchecked Sendable {} +extension Config.SessionkeyConfig: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -1801,6 +1819,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), 8: .same(proto: "security"), + 9: .same(proto: "sessionkey"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1913,6 +1932,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .security(v) } }() + case 9: try { + var v: Config.SessionkeyConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .sessionkey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .sessionkey(v) + } + }() default: break } } @@ -1956,6 +1988,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .security(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 8) }() + case .sessionkey?: try { + guard case .sessionkey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1973,7 +2009,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "role"), 2: .standard(proto: "serial_enabled"), - 3: .standard(proto: "debug_log_enabled"), 4: .standard(proto: "button_gpio"), 5: .standard(proto: "buzzer_gpio"), 6: .standard(proto: "rebroadcast_mode"), @@ -1993,7 +2028,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 2: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.debugLogEnabled) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.buttonGpio) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() @@ -2015,9 +2049,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.serialEnabled != false { try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 2) } - if self.debugLogEnabled != false { - try visitor.visitSingularBoolField(value: self.debugLogEnabled, fieldNumber: 3) - } if self.buttonGpio != 0 { try visitor.visitSingularUInt32Field(value: self.buttonGpio, fieldNumber: 4) } @@ -2051,7 +2082,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static func ==(lhs: Config.DeviceConfig, rhs: Config.DeviceConfig) -> Bool { if lhs.role != rhs.role {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} - if lhs.debugLogEnabled != rhs.debugLogEnabled {return false} if lhs.buttonGpio != rhs.buttonGpio {return false} if lhs.buzzerGpio != rhs.buzzerGpio {return false} if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} @@ -2595,6 +2625,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 15: .standard(proto: "pa_fan_disabled"), 103: .standard(proto: "ignore_incoming"), 104: .standard(proto: "ignore_mqtt"), + 105: .standard(proto: "config_ok_to_mqtt"), ] fileprivate class _StorageClass { @@ -2615,6 +2646,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _paFanDisabled: Bool = false var _ignoreIncoming: [UInt32] = [] var _ignoreMqtt: Bool = false + var _configOkToMqtt: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -2646,6 +2678,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem _paFanDisabled = source._paFanDisabled _ignoreIncoming = source._ignoreIncoming _ignoreMqtt = source._ignoreMqtt + _configOkToMqtt = source._configOkToMqtt } } @@ -2681,6 +2714,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 15: try { try decoder.decodeSingularBoolField(value: &_storage._paFanDisabled) }() case 103: try { try decoder.decodeRepeatedUInt32Field(value: &_storage._ignoreIncoming) }() case 104: try { try decoder.decodeSingularBoolField(value: &_storage._ignoreMqtt) }() + case 105: try { try decoder.decodeSingularBoolField(value: &_storage._configOkToMqtt) }() default: break } } @@ -2740,6 +2774,9 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._ignoreMqtt != false { try visitor.visitSingularBoolField(value: _storage._ignoreMqtt, fieldNumber: 104) } + if _storage._configOkToMqtt != false { + try visitor.visitSingularBoolField(value: _storage._configOkToMqtt, fieldNumber: 105) + } } try unknownFields.traverse(visitor: &visitor) } @@ -2766,6 +2803,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._paFanDisabled != rhs_storage._paFanDisabled {return false} if _storage._ignoreIncoming != rhs_storage._ignoreIncoming {return false} if _storage._ignoreMqtt != rhs_storage._ignoreMqtt {return false} + if _storage._configOkToMqtt != rhs_storage._configOkToMqtt {return false} return true } if !storagesAreEqual {return false} @@ -2819,7 +2857,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI 1: .same(proto: "enabled"), 2: .same(proto: "mode"), 3: .standard(proto: "fixed_pin"), - 4: .standard(proto: "device_logging_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2831,7 +2868,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.mode) }() case 3: try { try decoder.decodeSingularUInt32Field(value: &self.fixedPin) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.deviceLoggingEnabled) }() default: break } } @@ -2847,9 +2883,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if self.fixedPin != 0 { try visitor.visitSingularUInt32Field(value: self.fixedPin, fieldNumber: 3) } - if self.deviceLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.deviceLoggingEnabled, fieldNumber: 4) - } try unknownFields.traverse(visitor: &visitor) } @@ -2857,7 +2890,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if lhs.enabled != rhs.enabled {return false} if lhs.mode != rhs.mode {return false} if lhs.fixedPin != rhs.fixedPin {return false} - if lhs.deviceLoggingEnabled != rhs.deviceLoggingEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2880,7 +2912,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 4: .standard(proto: "is_managed"), 5: .standard(proto: "serial_enabled"), 6: .standard(proto: "debug_log_api_enabled"), - 7: .standard(proto: "bluetooth_logging_enabled"), 8: .standard(proto: "admin_channel_enabled"), ] @@ -2892,11 +2923,10 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm switch fieldNumber { case 1: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() case 2: try { try decoder.decodeSingularBytesField(value: &self.privateKey) }() - case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() + case 3: try { try decoder.decodeRepeatedBytesField(value: &self.adminKey) }() case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogApiEnabled) }() - case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() default: break } @@ -2911,7 +2941,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm try visitor.visitSingularBytesField(value: self.privateKey, fieldNumber: 2) } if !self.adminKey.isEmpty { - try visitor.visitSingularBytesField(value: self.adminKey, fieldNumber: 3) + try visitor.visitRepeatedBytesField(value: self.adminKey, fieldNumber: 3) } if self.isManaged != false { try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 4) @@ -2922,9 +2952,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.debugLogApiEnabled != false { try visitor.visitSingularBoolField(value: self.debugLogApiEnabled, fieldNumber: 6) } - if self.bluetoothLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) - } if self.adminChannelEnabled != false { try visitor.visitSingularBoolField(value: self.adminChannelEnabled, fieldNumber: 8) } @@ -2938,9 +2965,27 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.isManaged != rhs.isManaged {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} if lhs.debugLogApiEnabled != rhs.debugLogApiEnabled {return false} - if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } + +extension Config.SessionkeyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Config.protoMessageName + ".SessionkeyConfig" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Config.SessionkeyConfig, rhs: Config.SessionkeyConfig) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index ba12908d..c7e8dc07 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -346,6 +346,19 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. case me25Ls014Y10Td // = 75 + /// + /// RP2040_FEATHER_RFM95 + /// Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED + /// https://www.adafruit.com/product/5714 + /// https://www.adafruit.com/product/326 + /// https://www.adafruit.com/product/938 + /// ^^^ short A0 to switch to I2C address 0x3C + case rp2040FeatherRfm95 // = 76 + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + case m5StackCorebasic // = 77 + case m5StackCore2 // = 78 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -434,6 +447,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 73: self = .wioE5 case 74: self = .radiomaster900Bandit case 75: self = .me25Ls014Y10Td + case 76: self = .rp2040FeatherRfm95 + case 77: self = .m5StackCorebasic + case 78: self = .m5StackCore2 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -516,6 +532,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .wioE5: return 73 case .radiomaster900Bandit: return 74 case .me25Ls014Y10Td: return 75 + case .rp2040FeatherRfm95: return 76 + case .m5StackCorebasic: return 77 + case .m5StackCore2: return 78 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -603,6 +622,9 @@ extension HardwareModel: CaseIterable { .wioE5, .radiomaster900Bandit, .me25Ls014Y10Td, + .rp2040FeatherRfm95, + .m5StackCorebasic, + .m5StackCore2, .privateHw, ] } @@ -1520,9 +1542,22 @@ public struct DataMessage { /// a message a heart or poop emoji. public var emoji: UInt32 = 0 + /// + /// Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. + public var bitfield: UInt32 { + get {return _bitfield ?? 0} + set {_bitfield = newValue} + } + /// Returns true if `bitfield` has been explicitly set. + public var hasBitfield: Bool {return self._bitfield != nil} + /// Clears the value of `bitfield`. Subsequent reads from it will return its default value. + public mutating func clearBitfield() {self._bitfield = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _bitfield: UInt32? = nil } /// @@ -1907,6 +1942,15 @@ public struct MeshPacket { /// assume it is important and use a slightly higher priority case reliable // = 70 + /// + /// If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. + /// Furthermore, responses stop relaying packets directed to a node early. + case response // = 80 + + /// + /// Higher priority for specific message types (portnums) to distinguish between other reliable packets. + case high // = 100 + /// /// Ack/naks are sent with very high priority to ensure that retransmission /// stops as soon as possible @@ -1928,6 +1972,8 @@ public struct MeshPacket { case 10: self = .background case 64: self = .default case 70: self = .reliable + case 80: self = .response + case 100: self = .high case 120: self = .ack case 127: self = .max default: self = .UNRECOGNIZED(rawValue) @@ -1941,6 +1987,8 @@ public struct MeshPacket { case .background: return 10 case .default: return 64 case .reliable: return 70 + case .response: return 80 + case .high: return 100 case .ack: return 120 case .max: return 127 case .UNRECOGNIZED(let i): return i @@ -2006,6 +2054,8 @@ extension MeshPacket.Priority: CaseIterable { .background, .default, .reliable, + .response, + .high, .ack, .max, ] @@ -3241,6 +3291,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 73: .same(proto: "WIO_E5"), 74: .same(proto: "RADIOMASTER_900_BANDIT"), 75: .same(proto: "ME25LS01_4Y10TD"), + 76: .same(proto: "RP2040_FEATHER_RFM95"), + 77: .same(proto: "M5STACK_COREBASIC"), + 78: .same(proto: "M5STACK_CORE2"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3779,6 +3832,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 6: .standard(proto: "request_id"), 7: .standard(proto: "reply_id"), 8: .same(proto: "emoji"), + 9: .same(proto: "bitfield"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3795,12 +3849,17 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 6: try { try decoder.decodeSingularFixed32Field(value: &self.requestID) }() case 7: try { try decoder.decodeSingularFixed32Field(value: &self.replyID) }() case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self._bitfield) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if self.portnum != .unknownApp { try visitor.visitSingularEnumField(value: self.portnum, fieldNumber: 1) } @@ -3825,6 +3884,9 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if self.emoji != 0 { try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8) } + try { if let v = self._bitfield { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3837,6 +3899,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if lhs.requestID != rhs.requestID {return false} if lhs.replyID != rhs.replyID {return false} if lhs.emoji != rhs.emoji {return false} + if lhs._bitfield != rhs._bitfield {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4224,6 +4287,8 @@ extension MeshPacket.Priority: SwiftProtobuf._ProtoNameProviding { 10: .same(proto: "BACKGROUND"), 64: .same(proto: "DEFAULT"), 70: .same(proto: "RELIABLE"), + 80: .same(proto: "RESPONSE"), + 100: .same(proto: "HIGH"), 120: .same(proto: "ACK"), 127: .same(proto: "MAX"), ] diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index e4b9ee08..dc1d6cce 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -140,6 +140,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) case max17048 // = 28 + + /// + /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor + case customSensor // = 29 case UNRECOGNIZED(Int) public init() { @@ -177,6 +181,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 26: self = .bmp3Xx case 27: self = .icm20948 case 28: self = .max17048 + case 29: self = .customSensor default: self = .UNRECOGNIZED(rawValue) } } @@ -212,6 +217,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .bmp3Xx: return 26 case .icm20948: return 27 case .max17048: return 28 + case .customSensor: return 29 case .UNRECOGNIZED(let i): return i } } @@ -252,6 +258,7 @@ extension TelemetrySensorType: CaseIterable { .bmp3Xx, .icm20948, .max17048, + .customSensor, ] } @@ -1003,6 +1010,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 26: .same(proto: "BMP3XX"), 27: .same(proto: "ICM20948"), 28: .same(proto: "MAX17048"), + 29: .same(proto: "CUSTOM_SENSOR"), ] } diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 154edf9d..06099bd1 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -38,7 +38,7 @@ struct WidgetsLiveActivity: Widget { .fixedSize() Spacer() } - if context.state.nodesOnline >= 100 { + if context.state.totalNodes >= 100 { Text("100+ online") .font(.caption) .foregroundStyle(.secondary) @@ -81,7 +81,7 @@ struct WidgetsLiveActivity: Widget { .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Bad \(context.state.badReceivedPackets)") + Text("Bad: \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -97,7 +97,7 @@ struct WidgetsLiveActivity: Widget { } compactLeading: { Image("m-logo-black") .resizable() - .frame(width: 30.0) + .frame(width: 25) .padding(4) .background(.green.gradient, in: ContainerRelativeShape()) } compactTrailing: { @@ -120,27 +120,26 @@ struct WidgetsLiveActivity: Widget { } } -// struct WidgetsLiveActivity_Previews: PreviewProvider { -// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") -// static let state = MeshActivityAttributes.ContentState( -// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) -// -// static var previews: some View { -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.compact)) -// .previewDisplayName("Compact") -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.minimal)) -// .previewDisplayName("Minimal") -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.expanded)) -// .previewDisplayName("Expanded") -// attributes -// .previewContext(state, viewKind: .content) -// .previewDisplayName("Notification") -// } -// } +struct WidgetsLiveActivity_Previews: PreviewProvider { + static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") + static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) + + static var previews: some View { + attributes + .previewContext(state, viewKind: .dynamicIsland(.compact)) + .previewDisplayName("Compact") + attributes + .previewContext(state, viewKind: .dynamicIsland(.minimal)) + .previewDisplayName("Minimal") + attributes + .previewContext(state, viewKind: .dynamicIsland(.expanded)) + .previewDisplayName("Expanded") + attributes + .previewContext(state, viewKind: .content) + .previewDisplayName("Notification") + } +} struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.isLuminanceReduced) var isLuminanceReduced @@ -192,6 +191,7 @@ struct NodeInfoView: View { var timerRange: ClosedRange var body: some View { + let errorRate = (Double(badReceivedPackets) / Double(receivedPackets)) * 100 VStack(alignment: .leading, spacing: 0) { Text(nodeName) .font(nodeName.count > 14 ? .callout : .title3) @@ -203,19 +203,13 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Packets Sent: \(sentPackets)") + Text("Packets: Sent \(sentPackets) Rec. \(receivedPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Packets Received: \(receivedPackets)") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - Text("Bad Packets: \(badReceivedPackets)") + Text("Bad: \(badReceivedPackets) \(String(format: "Error Rate: %.2f", errorRate))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) diff --git a/protobufs b/protobufs index b6237629..0acaec6e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit b623762940ebdb1887a3b31b86f4d9cdaa7e6ecf +Subproject commit 0acaec6eff00e748beeae89148093221f131cd9c